diff --git a/ACViewer/Commands/RelayCommand.cs b/ACViewer/Commands/RelayCommand.cs new file mode 100644 index 0000000..76aeef6 --- /dev/null +++ b/ACViewer/Commands/RelayCommand.cs @@ -0,0 +1,33 @@ +using System; +using System.Windows.Input; + +namespace ACViewer.Commands +{ + public class RelayCommand : ICommand + { + private readonly Action _execute; + private readonly Func _canExecute; + + public event EventHandler CanExecuteChanged + { + add { CommandManager.RequerySuggested += value; } + remove { CommandManager.RequerySuggested -= value; } + } + + public RelayCommand(Action execute, Func canExecute = null) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _canExecute = canExecute; + } + + public bool CanExecute(object parameter) + { + return _canExecute == null || _canExecute(parameter); + } + + public void Execute(object parameter) + { + _execute(parameter); + } + } +} \ No newline at end of file diff --git a/ACViewer/Config/Config.cs b/ACViewer/Config/Config.cs index d7318cd..10d69f4 100644 --- a/ACViewer/Config/Config.cs +++ b/ACViewer/Config/Config.cs @@ -10,5 +10,11 @@ public class Config public BackgroundColors BackgroundColors { get; set; } = new BackgroundColors(); public string Theme { get; set; } public Mouse Mouse { get; set; } = new Mouse(); + public KeyBindingConfig KeyBindingConfig { get; set; } + + public Config() + { + KeyBindingConfig = new KeyBindingConfig(); + } } } diff --git a/ACViewer/Config/ConfigManager.cs b/ACViewer/Config/ConfigManager.cs index 0d89599..d8e7b14 100644 --- a/ACViewer/Config/ConfigManager.cs +++ b/ACViewer/Config/ConfigManager.cs @@ -1,14 +1,18 @@ -using System; +// File: ACViewer/Config/ConfigManager.cs +using System; +using System.Collections.Generic; using System.IO; - +using System.Windows; +using ACViewer.Entity; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace ACViewer.Config { public static class ConfigManager { private static readonly string Filename = "ACViewer.json"; - + private static readonly string KeybindingsFile = "keybindings.json"; private static Config config { get; set; } public static Config Config @@ -17,23 +21,25 @@ public static Config Config { if (config == null) config = new Config(); - return config; } } - - public static Config Snapshot { get; set; } - - public static void SaveConfig() + + private static JsonSerializerSettings GetSerializerSettings() { - var settings = new JsonSerializerSettings(); - settings.Formatting = Formatting.Indented; - - var json = JsonConvert.SerializeObject(config, settings); - - File.WriteAllText(Filename, json); + return new JsonSerializerSettings + { + Formatting = Formatting.Indented, + TypeNameHandling = TypeNameHandling.Auto, + Converters = new JsonConverter[] + { + new StringEnumConverter() + } + }; } + public static Config Snapshot { get; set; } + public static void LoadConfig() { config = ReadConfig(); @@ -41,10 +47,10 @@ public static void LoadConfig() public static Config ReadConfig() { - if (!File.Exists(Filename)) return null; + if (!File.Exists(Filename)) + return null; var json = File.ReadAllText(Filename); - var _config = JsonConvert.DeserializeObject(json); if (_config == null) @@ -52,6 +58,7 @@ public static Config ReadConfig() Console.WriteLine($"ConfigManager.LoadConfig() - failed to parse {Filename}"); return null; } + return _config; } @@ -69,12 +76,125 @@ public static bool HasDBInfo { get { - return config != null && config.Database != null && !string.IsNullOrWhiteSpace(config.Database.Host) && - config.Database.Port > 0 && - !string.IsNullOrWhiteSpace(config.Database.DatabaseName) && - !string.IsNullOrWhiteSpace(config.Database.Username) && - !string.IsNullOrWhiteSpace(config.Database.Password); + return config != null && + config.Database != null && + !string.IsNullOrWhiteSpace(config.Database.Host) && + config.Database.Port > 0 && + !string.IsNullOrWhiteSpace(config.Database.DatabaseName) && + !string.IsNullOrWhiteSpace(config.Database.Username) && + !string.IsNullOrWhiteSpace(config.Database.Password); + } + } + + public static bool SaveKeyBindings() + { + try + { + var bindings = Config.KeyBindingConfig; + System.Diagnostics.Debug.WriteLine($"SaveKeyBindings - ToggleZLevel: {bindings.ToggleZLevel.MainKey} (Value: {bindings.ToggleZLevel.KeyValue})"); + + // Create verification copy + var verifyConfig = new KeyBindingConfig(); + CopyBindings(bindings, verifyConfig); + + var settings = GetSerializerSettings(); + var json = JsonConvert.SerializeObject(verifyConfig, settings); + + // Test deserialize + var testConfig = JsonConvert.DeserializeObject(json, settings); + System.Diagnostics.Debug.WriteLine($"Test deserialize ToggleZLevel: {testConfig.ToggleZLevel.MainKey} (Value: {testConfig.ToggleZLevel.KeyValue})"); + + // Verify both the key enum and the numeric value match + if (testConfig.ToggleZLevel.MainKey != bindings.ToggleZLevel.MainKey || + testConfig.ToggleZLevel.KeyValue != bindings.ToggleZLevel.KeyValue) + { + throw new Exception($"Verification failed - key mismatch after serialization test." + + $" Expected {bindings.ToggleZLevel.MainKey} ({bindings.ToggleZLevel.KeyValue})," + + $" got {testConfig.ToggleZLevel.MainKey} ({testConfig.ToggleZLevel.KeyValue})"); + } + + File.WriteAllText(KeybindingsFile, json); + SaveConfig(); + return true; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error saving keybindings: {ex}"); + return false; + } + } + + private static void CopyBindings(KeyBindingConfig source, KeyBindingConfig target) + { + target.MoveForward = source.MoveForward.Clone(); + target.MoveBackward = source.MoveBackward.Clone(); + target.StrafeLeft = source.StrafeLeft.Clone(); + target.StrafeRight = source.StrafeRight.Clone(); + target.MoveUp = source.MoveUp.Clone(); + target.MoveDown = source.MoveDown.Clone(); + target.ToggleZLevel = source.ToggleZLevel.Clone(); + target.IncreaseZLevel = source.IncreaseZLevel.Clone(); + target.DecreaseZLevel = source.DecreaseZLevel.Clone(); + + target.CustomBindings = new Dictionary(); + foreach (var kvp in source.CustomBindings) + { + target.CustomBindings[kvp.Key] = kvp.Value.Clone(); + } + } + + public static void SaveConfig() + { + try + { + var settings = GetSerializerSettings(); + var json = JsonConvert.SerializeObject(Config, settings); + File.WriteAllText(Filename, json); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error saving config: {ex}"); + throw; + } + } + + public static bool LoadKeyBindings() + { + try + { + if (!File.Exists(KeybindingsFile)) + { + Config.KeyBindingConfig = new KeyBindingConfig(); + Config.KeyBindingConfig.SetDefaults(); + return true; + } + + var json = File.ReadAllText(KeybindingsFile); + System.Diagnostics.Debug.WriteLine($"Loading keybindings: {json}"); + + var settings = GetSerializerSettings(); + var bindings = JsonConvert.DeserializeObject(json, settings); + + if (bindings != null) + { + // Create a new config and copy values to ensure clean state + var newConfig = new KeyBindingConfig(); + CopyBindings(bindings, newConfig); + newConfig.ValidateConfig(); + + Config.KeyBindingConfig = newConfig; + return true; + } + + return false; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error loading keybindings: {ex}"); + Config.KeyBindingConfig = new KeyBindingConfig(); + Config.KeyBindingConfig.SetDefaults(); + return false; } } } -} +} \ No newline at end of file diff --git a/ACViewer/Config/KeyBindingConfig.cs b/ACViewer/Config/KeyBindingConfig.cs new file mode 100644 index 0000000..cacbc19 --- /dev/null +++ b/ACViewer/Config/KeyBindingConfig.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Windows.Input; +using Microsoft.Xna.Framework.Input; +using Newtonsoft.Json; +using ACViewer.Entity; + +namespace ACViewer.Config +{ + public class KeyBindingConfig + { + // Camera Controls + public GameKeyBinding MoveForward { get; set; } + public GameKeyBinding MoveBackward { get; set; } + public GameKeyBinding StrafeLeft { get; set; } + public GameKeyBinding StrafeRight { get; set; } + public GameKeyBinding MoveUp { get; set; } + public GameKeyBinding MoveDown { get; set; } + + // Z-Level Controls + public GameKeyBinding ToggleZLevel { get; set; } + public GameKeyBinding IncreaseZLevel { get; set; } + public GameKeyBinding DecreaseZLevel { get; set; } + + // Custom bindings for extensibility + public Dictionary CustomBindings { get; set; } + private readonly Dictionary _actionBindings = new(); + + public KeyBindingConfig() + { + CustomBindings = new Dictionary(); + SetDefaults(); + } + + public void SetDefaults() + { + // Camera controls + MoveForward = new GameKeyBinding(Keys.W, ModifierKeys.None, "Move Forward", "Camera"); + MoveBackward = new GameKeyBinding(Keys.S, ModifierKeys.None, "Move Backward", "Camera"); + StrafeLeft = new GameKeyBinding(Keys.A, ModifierKeys.None, "Strafe Left", "Camera"); + StrafeRight = new GameKeyBinding(Keys.D, ModifierKeys.None, "Strafe Right", "Camera"); + MoveUp = new GameKeyBinding(Keys.Space, ModifierKeys.None, "Move Up", "Camera"); + MoveDown = new GameKeyBinding(Keys.LeftShift, ModifierKeys.None, "Move Down", "Camera"); + + // Z-Level controls + ToggleZLevel = new GameKeyBinding(Keys.F3, ModifierKeys.None, "Toggle Z-Level", "Z-Level"); + IncreaseZLevel = new GameKeyBinding(Keys.OemPlus, ModifierKeys.Alt, "Increase Z-Level", "Z-Level"); + DecreaseZLevel = new GameKeyBinding(Keys.OemMinus, ModifierKeys.Alt, "Decrease Z-Level", "Z-Level"); + } + + public void ValidateConfig() + { + // Ensure no null bindings + if (MoveForward == null) MoveForward = new GameKeyBinding(); + if (MoveBackward == null) MoveBackward = new GameKeyBinding(); + if (StrafeLeft == null) StrafeLeft = new GameKeyBinding(); + if (StrafeRight == null) StrafeRight = new GameKeyBinding(); + if (MoveUp == null) MoveUp = new GameKeyBinding(); + if (MoveDown == null) MoveDown = new GameKeyBinding(); + if (ToggleZLevel == null) ToggleZLevel = new GameKeyBinding(); + if (IncreaseZLevel == null) IncreaseZLevel = new GameKeyBinding(); + if (DecreaseZLevel == null) DecreaseZLevel = new GameKeyBinding(); + if (CustomBindings == null) CustomBindings = new Dictionary(); + } + + public GameKeyBinding GetBindingForAction(string actionName) + { + switch (actionName) + { + case "Move Forward": return MoveForward; + case "Move Backward": return MoveBackward; + case "Strafe Left": return StrafeLeft; + case "Strafe Right": return StrafeRight; + case "Move Up": return MoveUp; + case "Move Down": return MoveDown; + case "Toggle Z-Level": return ToggleZLevel; + case "Increase Z-Level": return IncreaseZLevel; + case "Decrease Z-Level": return DecreaseZLevel; + } + + if (_actionBindings.TryGetValue(actionName, out var binding)) + return binding; + + return new GameKeyBinding(); + } + + public void SetBindingForAction(string actionName, GameKeyBinding binding) +{ + System.Diagnostics.Debug.WriteLine($"SetBindingForAction - Name: {actionName}, Key: {binding.MainKey}, Modifiers: {binding.Modifiers}"); + + switch (actionName) + { + case "Move Forward": + MoveForward = binding; + System.Diagnostics.Debug.WriteLine($"Set MoveForward to {binding.MainKey}"); + break; + case "Move Backward": + MoveBackward = binding; + System.Diagnostics.Debug.WriteLine($"Set MoveBackward to {binding.MainKey}"); + break; + case "Strafe Left": + StrafeLeft = binding; + System.Diagnostics.Debug.WriteLine($"Set StrafeLeft to {binding.MainKey}"); + break; + case "Strafe Right": + StrafeRight = binding; + System.Diagnostics.Debug.WriteLine($"Set StrafeRight to {binding.MainKey}"); + break; + case "Move Up": + MoveUp = binding; + System.Diagnostics.Debug.WriteLine($"Set MoveUp to {binding.MainKey}"); + break; + case "Move Down": + MoveDown = binding; + System.Diagnostics.Debug.WriteLine($"Set MoveDown to {binding.MainKey}"); + break; + case "Toggle Z-Level": + ToggleZLevel = binding; + System.Diagnostics.Debug.WriteLine($"Set ToggleZLevel to {binding.MainKey}"); + break; + case "Increase Z-Level": + IncreaseZLevel = binding; + System.Diagnostics.Debug.WriteLine($"Set IncreaseZLevel to {binding.MainKey}"); + break; + case "Decrease Z-Level": + DecreaseZLevel = binding; + System.Diagnostics.Debug.WriteLine($"Set DecreaseZLevel to {binding.MainKey}"); + break; + default: + CustomBindings[actionName] = binding; + System.Diagnostics.Debug.WriteLine($"Set CustomBinding {actionName} to {binding.MainKey}"); + break; + } +} + + public void ExportToFile(string filePath) + { + try + { + var settings = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + TypeNameHandling = TypeNameHandling.Auto + }; + + var json = JsonConvert.SerializeObject(this, settings); + File.WriteAllText(filePath, json); + } + catch (Exception ex) + { + throw new Exception("Failed to export keybindings", ex); + } + } + + public static KeyBindingConfig ImportFromFile(string filePath) + { + try + { + var json = File.ReadAllText(filePath); + var settings = new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.Auto + }; + + var config = JsonConvert.DeserializeObject(json, settings); + config.ValidateConfig(); + return config; + } + catch (Exception ex) + { + throw new Exception("Failed to import keybindings", ex); + } + } + + public static KeyBindingConfig CreateProfile(string profileType) + { + var config = new KeyBindingConfig(); + + switch (profileType.ToLower()) + { + case "default": + // Already handled by constructor + break; + + case "alternative": + // Alternative ESDF layout + config.MoveForward = new GameKeyBinding(Keys.E, ModifierKeys.None, "Move Forward", "Camera"); + config.MoveBackward = new GameKeyBinding(Keys.D, ModifierKeys.None, "Move Backward", "Camera"); + config.StrafeLeft = new GameKeyBinding(Keys.S, ModifierKeys.None, "Strafe Left", "Camera"); + config.StrafeRight = new GameKeyBinding(Keys.F, ModifierKeys.None, "Strafe Right", "Camera"); + config.MoveUp = new GameKeyBinding(Keys.Space, ModifierKeys.None, "Move Up", "Camera"); + config.MoveDown = new GameKeyBinding(Keys.LeftControl, ModifierKeys.None, "Move Down", "Camera"); + break; + + case "arrows": + // Arrow key layout + config.MoveForward = new GameKeyBinding(Keys.Up, ModifierKeys.None, "Move Forward", "Camera"); + config.MoveBackward = new GameKeyBinding(Keys.Down, ModifierKeys.None, "Move Backward", "Camera"); + config.StrafeLeft = new GameKeyBinding(Keys.Left, ModifierKeys.None, "Strafe Left", "Camera"); + config.StrafeRight = new GameKeyBinding(Keys.Right, ModifierKeys.None, "Strafe Right", "Camera"); + config.MoveUp = new GameKeyBinding(Keys.PageUp, ModifierKeys.None, "Move Up", "Camera"); + config.MoveDown = new GameKeyBinding(Keys.PageDown, ModifierKeys.None, "Move Down", "Camera"); + break; + + default: + throw new ArgumentException($"Unknown profile type: {profileType}"); + } + + return config; + } + } +} \ No newline at end of file diff --git a/ACViewer/Config/MapViewerOptions.cs b/ACViewer/Config/MapViewerOptions.cs index 6daf1a9..ed40d08 100644 --- a/ACViewer/Config/MapViewerOptions.cs +++ b/ACViewer/Config/MapViewerOptions.cs @@ -5,6 +5,9 @@ namespace ACViewer.Config public class MapViewerOptions { public MapViewerMode Mode { get; set; } + public bool EnableZSlicing { get; set; } = false; + public int CurrentZLevel { get; set; } = 1; + public float LevelHeight { get; set; } = 10.0f; public MapViewerOptions() { diff --git a/ACViewer/Entity/GameKeyBinding.cs b/ACViewer/Entity/GameKeyBinding.cs new file mode 100644 index 0000000..a618d1b --- /dev/null +++ b/ACViewer/Entity/GameKeyBinding.cs @@ -0,0 +1,72 @@ +using Microsoft.Xna.Framework.Input; +using System; +using System.Windows.Input; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace ACViewer.Entity +{ + public class GameKeyBinding + { + [JsonProperty("MainKey")] + [JsonConverter(typeof(StringEnumConverter))] + public Keys MainKey { get; set; } + + [JsonProperty("Modifiers")] + [JsonConverter(typeof(StringEnumConverter))] + public ModifierKeys Modifiers { get; set; } + + [JsonProperty("DisplayName")] + public string DisplayName { get; set; } + + [JsonProperty("Category")] + public string Category { get; set; } + + [JsonProperty("KeyValue")] + public int KeyValue { get; set; } + + [JsonIgnore] + public bool IsEmpty => MainKey == Keys.None; + + public GameKeyBinding(Keys mainKey = Keys.None, ModifierKeys modifiers = ModifierKeys.None, + string displayName = "", string category = "") + { + MainKey = mainKey; + KeyValue = (int)mainKey; // Store the actual numeric value + Modifiers = modifiers; + DisplayName = displayName; + Category = category; + + System.Diagnostics.Debug.WriteLine($"Creating new GameKeyBinding - Key: {mainKey}, Value: {KeyValue}"); + } + + public GameKeyBinding Clone() + { + System.Diagnostics.Debug.WriteLine($"Cloning GameKeyBinding - Key: {MainKey}, Value: {KeyValue}"); + var clone = new GameKeyBinding + { + MainKey = this.MainKey, + KeyValue = this.KeyValue, // Preserve the actual key value + Modifiers = this.Modifiers, + DisplayName = this.DisplayName, + Category = this.Category + }; + System.Diagnostics.Debug.WriteLine($"Cloned binding - Key: {clone.MainKey}, Value: {clone.KeyValue}"); + return clone; + } + + public bool Matches(KeyboardState state, ModifierKeys currentModifiers) + { + if (IsEmpty) return false; + if (!state.IsKeyDown(MainKey)) return false; + return currentModifiers == Modifiers; + } + + public string GetDisplayString() + { + if (IsEmpty) return "None"; + var modifierStr = Modifiers != ModifierKeys.None ? $"{Modifiers}+" : ""; + return $"{modifierStr}{MainKey}"; + } + } +} \ No newline at end of file diff --git a/ACViewer/Extensions/CommandHandler.cs b/ACViewer/Extensions/CommandHandler.cs index 2ab31e4..3ef602e 100644 --- a/ACViewer/Extensions/CommandHandler.cs +++ b/ACViewer/Extensions/CommandHandler.cs @@ -1,5 +1,6 @@ using System; using System.Windows.Input; +using ACViewer.Config; namespace ACViewer.Extensions { diff --git a/ACViewer/Extensions/Vector3Extensions.cs b/ACViewer/Extensions/Vector3Extensions.cs index 808c1f2..de74dc0 100644 --- a/ACViewer/Extensions/Vector3Extensions.cs +++ b/ACViewer/Extensions/Vector3Extensions.cs @@ -1,11 +1,16 @@ using System; +using Microsoft.Xna.Framework; + +using System.Numerics; + using ACE.Server.Physics; namespace ACViewer { public static class Vector3Extensions { + public static System.Numerics.Vector3 ToNumerics(this Microsoft.Xna.Framework.Vector3 v) { return new System.Numerics.Vector3(v.X, v.Y, v.Z); diff --git a/ACViewer/GameView.cs b/ACViewer/GameView.cs index c6a2dd3..7bcb581 100644 --- a/ACViewer/GameView.cs +++ b/ACViewer/GameView.cs @@ -21,6 +21,8 @@ public class GameView : WpfGame public WpfKeyboard _keyboard { get; set; } public WpfMouse _mouse { get; set; } + + public Input.InputManager InputManager { get; private set; } public KeyboardState PrevKeyboardState { get; set; } public MouseState PrevMouseState { get; set; } @@ -73,13 +75,12 @@ public static ViewMode ViewMode // text rendering public SpriteFont Font { get; set; } - + protected override void Initialize() { // must be initialized. required by Content loading and rendering (will add itself to the Services) // note that MonoGame requires this to be initialized in the constructor, while WpfInterop requires it to // be called inside Initialize (before base.Initialize()) - //var dummy = new DummyView(); _graphicsDeviceManager = new WpfGraphicsDeviceService(this) { PreferMultiSampling = UseMSAA @@ -92,6 +93,15 @@ protected override void Initialize() _keyboard = new WpfKeyboard(this); _mouse = new WpfMouse(this); + // Initialize input manager + InputManager = new Input.InputManager(Config.ConfigManager.Config.KeyBindingConfig); + + // Load saved key bindings + if (Config.ConfigManager.LoadKeyBindings()) + { + InputManager.UpdateBindings(Config.ConfigManager.Config.KeyBindingConfig); + } + Instance = this; // must be called after the WpfGraphicsDeviceService instance was created @@ -99,6 +109,11 @@ protected override void Initialize() SizeChanged += new SizeChangedEventHandler(GameView_SizeChanged); } + + public void UpdateInputManager(Config.KeyBindingConfig newConfig) + { + InputManager?.UpdateBindings(newConfig); + } protected override void LoadContent() { diff --git a/ACViewer/Input/BindingValidator.cs b/ACViewer/Input/BindingValidator.cs new file mode 100644 index 0000000..1f7e70c --- /dev/null +++ b/ACViewer/Input/BindingValidator.cs @@ -0,0 +1,76 @@ +using ACViewer.Entity; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Input; +using Microsoft.Xna.Framework.Input; + +namespace ACViewer.Input +{ + public class BindingValidator + { + private readonly Dictionary> _categoryBindings; + + public BindingValidator() + { + _categoryBindings = new Dictionary>(); + } + + public void RegisterBinding(GameKeyBinding binding) + { + if (!_categoryBindings.ContainsKey(binding.Category)) + _categoryBindings[binding.Category] = new List(); + + _categoryBindings[binding.Category].Add(binding); + } + + public void UnregisterBinding(GameKeyBinding binding) + { + if (_categoryBindings.ContainsKey(binding.Category)) + _categoryBindings[binding.Category].Remove(binding); + } + + public (bool isValid, string error) ValidateBinding(GameKeyBinding newBinding) + { + if (newBinding.IsEmpty) + return (true, null); + + if (IsSystemReserved(newBinding)) + return (false, $"The combination {newBinding.GetDisplayString()} is reserved by the system"); + + var conflicts = FindConflicts(newBinding); + if (conflicts.Any()) + { + var conflictList = string.Join(", ", conflicts.Select(b => $"{b.DisplayName} ({b.GetDisplayString()})")); + return (false, $"Conflicts with existing bindings: {conflictList}"); + } + + return (true, null); + } + + private List FindConflicts(GameKeyBinding binding) + { + var conflicts = new List(); + + foreach (var categoryBindings in _categoryBindings.Values) + { + conflicts.AddRange(categoryBindings.Where(b => + b != binding && + b.MainKey == binding.MainKey && + b.Modifiers == binding.Modifiers)); + } + + return conflicts; + } + + private bool IsSystemReserved(GameKeyBinding binding) + { + if (binding.Modifiers.HasFlag(ModifierKeys.Alt)) + { + if (binding.MainKey == Keys.F4 || binding.MainKey == Keys.Tab) + return true; + } + + return false; + } + } +} diff --git a/ACViewer/Input/InputManager.cs b/ACViewer/Input/InputManager.cs new file mode 100644 index 0000000..967163a --- /dev/null +++ b/ACViewer/Input/InputManager.cs @@ -0,0 +1,81 @@ +using System; +using System.Windows.Input; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using ACViewer.Config; +using ACViewer.Entity; +using GameTime = Microsoft.Xna.Framework.GameTime; +using Keyboard = Microsoft.Xna.Framework.Input.Keyboard; + + +namespace ACViewer.Input +{ + public class InputManager + { + private KeyBindingConfig _config; + private KeyboardState _currentState; + private KeyboardState _previousState; + private ModifierKeys _currentModifiers; + + public event Action BindingPressed; + + public InputManager(KeyBindingConfig config) + { + _config = config; + _currentState = Keyboard.GetState(); + _previousState = _currentState; + } + + public void Update(GameTime gameTime) + { + _previousState = _currentState; + _currentState = Keyboard.GetState(); + UpdateModifiers(); + + CheckBinding(_config.MoveForward); + CheckBinding(_config.MoveBackward); + CheckBinding(_config.StrafeLeft); + CheckBinding(_config.StrafeRight); + CheckBinding(_config.MoveUp); + CheckBinding(_config.MoveDown); + CheckBinding(_config.ToggleZLevel); + CheckBinding(_config.IncreaseZLevel); + CheckBinding(_config.DecreaseZLevel); + + foreach (var binding in _config.CustomBindings.Values) + CheckBinding(binding); + } + + private void CheckBinding(GameKeyBinding binding) + { + if (binding.Matches(_currentState, _currentModifiers)) + BindingPressed?.Invoke(binding); + } + + private void UpdateModifiers() + { + _currentModifiers = ModifierKeys.None; + + if (_currentState.IsKeyDown(Keys.LeftShift) || _currentState.IsKeyDown(Keys.RightShift)) + _currentModifiers |= ModifierKeys.Shift; + + if (_currentState.IsKeyDown(Keys.LeftControl) || _currentState.IsKeyDown(Keys.RightControl)) + _currentModifiers |= ModifierKeys.Control; + + if (_currentState.IsKeyDown(Keys.LeftAlt) || _currentState.IsKeyDown(Keys.RightAlt)) + _currentModifiers |= ModifierKeys.Alt; + } + + public bool IsBindingActive(GameKeyBinding binding) + { + return binding.Matches(_currentState, _currentModifiers); + } + + public void UpdateBindings(KeyBindingConfig newConfig) + { + _config = newConfig; + } + + public ModifierKeys GetCurrentModifiers() => _currentModifiers; + } +} \ No newline at end of file diff --git a/ACViewer/Input/KeyBindingConflictDetector.cs b/ACViewer/Input/KeyBindingConflictDetector.cs new file mode 100644 index 0000000..451a24b --- /dev/null +++ b/ACViewer/Input/KeyBindingConflictDetector.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Linq; +using ACViewer.Config; +using ACViewer.Entity; + +namespace ACViewer.Input +{ + public class KeyBindingConflictDetector + { + public struct Conflict + { + public string Action1 { get; set; } + public string Action2 { get; set; } + public string DisplayString { get; set; } + + public Conflict(string action1, string action2, string displayString) + { + Action1 = action1; + Action2 = action2; + DisplayString = displayString; + } + } + + public static List DetectConflicts(KeyBindingConfig config) + { + var conflicts = new List(); + var allBindings = new List<(string Action, GameKeyBinding Binding)>(); + + // Collect all bindings + AddBinding("Move Forward", config.MoveForward); + AddBinding("Move Backward", config.MoveBackward); + AddBinding("Strafe Left", config.StrafeLeft); + AddBinding("Strafe Right", config.StrafeRight); + AddBinding("Move Up", config.MoveUp); + AddBinding("Move Down", config.MoveDown); + AddBinding("Toggle Z-Level", config.ToggleZLevel); + AddBinding("Increase Z-Level", config.IncreaseZLevel); + AddBinding("Decrease Z-Level", config.DecreaseZLevel); + + foreach (var customBinding in config.CustomBindings) + AddBinding(customBinding.Key, customBinding.Value); + + // Check for conflicts + for (int i = 0; i < allBindings.Count; i++) + { + for (int j = i + 1; j < allBindings.Count; j++) + { + var binding1 = allBindings[i]; + var binding2 = allBindings[j]; + + if (!binding1.Binding.IsEmpty && !binding2.Binding.IsEmpty && + binding1.Binding.MainKey == binding2.Binding.MainKey && + binding1.Binding.Modifiers == binding2.Binding.Modifiers) + { + conflicts.Add(new Conflict( + binding1.Action, + binding2.Action, + binding1.Binding.GetDisplayString() + )); + } + } + } + + return conflicts; + + void AddBinding(string action, GameKeyBinding binding) + { + if (binding != null) + allBindings.Add((action, binding)); + } + } + } +} \ No newline at end of file diff --git a/ACViewer/Input/KeyRecorder.cs b/ACViewer/Input/KeyRecorder.cs new file mode 100644 index 0000000..9e01cbc --- /dev/null +++ b/ACViewer/Input/KeyRecorder.cs @@ -0,0 +1,220 @@ +using System; +using System.Windows.Input; +using System.Windows.Threading; +using System.Windows; +using Microsoft.Xna.Framework.Input; +using ACViewer.Entity; +using Keyboard = System.Windows.Input.Keyboard; +using Keys = Microsoft.Xna.Framework.Input.Keys; + +namespace ACViewer.Input +{ + public class KeyRecorder + { + private bool _isRecording; + private ModifierKeys _currentModifiers; + private Window _parentWindow; + + public event Action OnKeyStateChanged; + public event Action RecordingComplete; + public event Action RecordingCancelled; + public event Action RecordingError; + + private readonly DispatcherTimer _timer; + private int _remainingTime; + private const int TIMEOUT_SECONDS = 3; + + public KeyRecorder() + { + _timer = new DispatcherTimer + { + Interval = TimeSpan.FromSeconds(1) + }; + _timer.Tick += Timer_Tick; + } + + private void Timer_Tick(object sender, EventArgs e) + { + if (!_isRecording) return; + + _remainingTime--; + System.Diagnostics.Debug.WriteLine($"Timer tick: {_remainingTime} seconds remaining"); + + if (_remainingTime <= 0) + { + StopRecording(); + RecordingCancelled?.Invoke(); + } + } + + public void StartRecording(Window parentWindow) + { + try + { + if (_isRecording) return; + + System.Diagnostics.Debug.WriteLine("Starting key recording..."); + _isRecording = true; + _parentWindow = parentWindow; + _remainingTime = TIMEOUT_SECONDS; + _currentModifiers = ModifierKeys.None; + + _parentWindow.PreviewKeyDown += Window_PreviewKeyDown; + _timer.Start(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error starting recording: {ex}"); + RecordingError?.Invoke($"Error starting recording: {ex.Message}"); + } + } + + public void StopRecording() + { + if (!_isRecording) return; + + System.Diagnostics.Debug.WriteLine("Stopping key recording..."); + _isRecording = false; + _timer.Stop(); + + if (_parentWindow != null) + { + _parentWindow.PreviewKeyDown -= Window_PreviewKeyDown; + _parentWindow = null; + } + } + + private void Window_PreviewKeyDown(object sender, KeyEventArgs e) + { + if (!_isRecording) return; + + System.Diagnostics.Debug.WriteLine($"Raw Key pressed: {e.Key}, System Key: {e.SystemKey}, Modifiers: {Keyboard.Modifiers}, Key Value: {(int)e.Key}"); + + _currentModifiers = Keyboard.Modifiers; + + if (e.Key == System.Windows.Input.Key.Escape) + { + e.Handled = true; + StopRecording(); + RecordingCancelled?.Invoke(); + return; + } + + var actualKey = e.Key == System.Windows.Input.Key.System ? e.SystemKey : e.Key; + + if (IsModifierKey(actualKey)) + { + OnKeyStateChanged?.Invoke(Keys.None, _currentModifiers); + e.Handled = true; + return; + } + + var xnaKey = ConvertToXNAKey(actualKey); + System.Diagnostics.Debug.WriteLine($"Converted to XNA Key: {xnaKey}"); + + if (xnaKey != Keys.None) + { + var binding = new GameKeyBinding(xnaKey, _currentModifiers); + System.Diagnostics.Debug.WriteLine($"Created binding - Key: {binding.MainKey}, Modifiers: {binding.Modifiers}"); + + if (ValidateBinding(binding)) + { + e.Handled = true; + StopRecording(); + RecordingComplete?.Invoke(binding); + } + } + + OnKeyStateChanged?.Invoke(xnaKey, _currentModifiers); + e.Handled = true; + } + + private Keys ConvertToXNAKey(System.Windows.Input.Key wpfKey) + { + System.Diagnostics.Debug.WriteLine($"Converting WPF Key: {wpfKey}"); + + // Direct function key mapping + if (wpfKey >= System.Windows.Input.Key.F1 && wpfKey <= System.Windows.Input.Key.F24) + { + var fKeyNumber = (int)wpfKey - (int)System.Windows.Input.Key.F1 + 1; + var xnaKey = (Keys)((int)Keys.F1 + (fKeyNumber - 1)); + System.Diagnostics.Debug.WriteLine($"Mapped F-key {fKeyNumber} to XNA Key: {xnaKey} (value: {(int)xnaKey})"); + return xnaKey; + } + + return wpfKey switch + { + System.Windows.Input.Key.A => Keys.A, + System.Windows.Input.Key.B => Keys.B, + System.Windows.Input.Key.C => Keys.C, + System.Windows.Input.Key.D => Keys.D, + System.Windows.Input.Key.E => Keys.E, + System.Windows.Input.Key.F => Keys.F, + System.Windows.Input.Key.G => Keys.G, + System.Windows.Input.Key.H => Keys.H, + System.Windows.Input.Key.I => Keys.I, + System.Windows.Input.Key.J => Keys.J, + System.Windows.Input.Key.K => Keys.K, + System.Windows.Input.Key.L => Keys.L, + System.Windows.Input.Key.M => Keys.M, + System.Windows.Input.Key.N => Keys.N, + System.Windows.Input.Key.O => Keys.O, + System.Windows.Input.Key.P => Keys.P, + System.Windows.Input.Key.Q => Keys.Q, + System.Windows.Input.Key.R => Keys.R, + System.Windows.Input.Key.S => Keys.S, + System.Windows.Input.Key.T => Keys.T, + System.Windows.Input.Key.U => Keys.U, + System.Windows.Input.Key.V => Keys.V, + System.Windows.Input.Key.W => Keys.W, + System.Windows.Input.Key.X => Keys.X, + System.Windows.Input.Key.Y => Keys.Y, + System.Windows.Input.Key.Z => Keys.Z, + System.Windows.Input.Key.D0 => Keys.D0, + System.Windows.Input.Key.D1 => Keys.D1, + System.Windows.Input.Key.D2 => Keys.D2, + System.Windows.Input.Key.D3 => Keys.D3, + System.Windows.Input.Key.D4 => Keys.D4, + System.Windows.Input.Key.D5 => Keys.D5, + System.Windows.Input.Key.D6 => Keys.D6, + System.Windows.Input.Key.D7 => Keys.D7, + System.Windows.Input.Key.D8 => Keys.D8, + System.Windows.Input.Key.D9 => Keys.D9, + System.Windows.Input.Key.Space => Keys.Space, + System.Windows.Input.Key.Enter => Keys.Enter, + System.Windows.Input.Key.Tab => Keys.Tab, + System.Windows.Input.Key.Up => Keys.Up, + System.Windows.Input.Key.Down => Keys.Down, + System.Windows.Input.Key.Left => Keys.Left, + System.Windows.Input.Key.Right => Keys.Right, + System.Windows.Input.Key.OemPlus => Keys.OemPlus, + System.Windows.Input.Key.OemMinus => Keys.OemMinus, + System.Windows.Input.Key.LeftShift => Keys.LeftShift, + System.Windows.Input.Key.RightShift => Keys.RightShift, + System.Windows.Input.Key.LeftCtrl => Keys.LeftControl, + System.Windows.Input.Key.RightCtrl => Keys.RightControl, + System.Windows.Input.Key.LeftAlt => Keys.LeftAlt, + System.Windows.Input.Key.RightAlt => Keys.RightAlt, + _ => Keys.None + }; + } + + private bool IsModifierKey(System.Windows.Input.Key key) + { + return key == System.Windows.Input.Key.LeftShift || key == System.Windows.Input.Key.RightShift || + key == System.Windows.Input.Key.LeftCtrl || key == System.Windows.Input.Key.RightCtrl || + key == System.Windows.Input.Key.LeftAlt || key == System.Windows.Input.Key.RightAlt; + } + + private bool ValidateBinding(GameKeyBinding binding) + { + if (binding.Modifiers.HasFlag(ModifierKeys.Alt) && binding.MainKey == Keys.F4) + { + RecordingError?.Invoke("Alt+F4 is reserved by the system"); + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/ACViewer/Model/ActionRegistry.cs b/ACViewer/Model/ActionRegistry.cs new file mode 100644 index 0000000..2e07444 --- /dev/null +++ b/ACViewer/Model/ActionRegistry.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; + +namespace ACViewer.Model +{ + public class ActionRegistry + { + private static ActionRegistry _instance; + public static ActionRegistry Instance => _instance ??= new ActionRegistry(); + + private readonly Dictionary _actions = new(); + private readonly Dictionary> _categories = new(); + + public void RegisterAction(UserAction action) + { + _actions[action.Name] = action; + + if (!_categories.ContainsKey(action.Category)) + _categories[action.Category] = new List(); + + _categories[action.Category].Add(action.Name); + } + + public UserAction GetAction(string name) + { + return _actions.TryGetValue(name, out var action) ? action : null; + } + + public IReadOnlyList GetCategories() + { + return _categories.Keys.ToList(); + } + + public IReadOnlyList GetActionsInCategory(string category) + { + if (!_categories.ContainsKey(category)) + return new List(); + + return _categories[category] + .Select(name => _actions[name]) + .ToList(); + } + + public void ExecuteAction(string name) + { + if (_actions.TryGetValue(name, out var action) && action.IsEnabled) + action.ExecuteAction?.Invoke(); + } + + public void Clear() + { + _actions.Clear(); + _categories.Clear(); + } + } +} \ No newline at end of file diff --git a/ACViewer/Model/UserAction.cs b/ACViewer/Model/UserAction.cs new file mode 100644 index 0000000..bb48c5d --- /dev/null +++ b/ACViewer/Model/UserAction.cs @@ -0,0 +1,21 @@ +using System; + +namespace ACViewer.Model +{ + public class UserAction + { + public string Name { get; set; } + public string Category { get; set; } + public Action ExecuteAction { get; set; } + public string Description { get; set; } + public bool IsEnabled { get; set; } = true; + + public UserAction(string name, string category, Action executeAction, string description = "") + { + Name = name; + Category = category; + ExecuteAction = executeAction; + Description = description; + } + } +} \ No newline at end of file diff --git a/ACViewer/Model/VertexInstance.cs b/ACViewer/Model/VertexInstance.cs index cbfa891..8c3a6d2 100644 --- a/ACViewer/Model/VertexInstance.cs +++ b/ACViewer/Model/VertexInstance.cs @@ -5,9 +5,10 @@ namespace ACViewer.Model { public struct VertexInstance : IVertexType { - public Vector3 Position; + public Vector3 Position { get; set; } public Vector4 Orientation; public Vector3 Scale; + public readonly static VertexDeclaration VertexDeclaration = new VertexDeclaration ( diff --git a/ACViewer/Render/Buffer.cs b/ACViewer/Render/Buffer.cs index 1afad3f..36b524a 100644 --- a/ACViewer/Render/Buffer.cs +++ b/ACViewer/Render/Buffer.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using ACE.DatLoader.Entity; using ACE.Server.Physics; - +using ACViewer.Config; +using ACViewer.Extensions; using ACViewer.Enum; using ACViewer.Model; @@ -86,6 +88,95 @@ public void Init() AnimatedTextureAtlasChains = new Dictionary(); } + + private bool IsInCurrentZLevel(Vector3 position) + { + if (!ConfigManager.Config.MapViewer.EnableZSlicing) + return true; + + var config = ConfigManager.Config.MapViewer; + float levelBottom = (config.CurrentZLevel - 1) * config.LevelHeight; + float levelTop = levelBottom + config.LevelHeight; + + return position.Z >= levelBottom && position.Z < levelTop; + } + + public void DrawWithZSlicing() + { + Effect.Parameters["xWorld"].SetValue(Matrix.Identity); + Effect.Parameters["xLightDirection"].SetValue(-Vector3.UnitZ); + Effect.Parameters["xAmbient"].SetValue(0.5f); + + Effect_Clamp.Parameters["xWorld"].SetValue(Matrix.Identity); + Effect_Clamp.Parameters["xLightDirection"].SetValue(-Vector3.UnitZ); + Effect_Clamp.Parameters["xAmbient"].SetValue(0.5f); + + PerfTimer.Start(ProfilerSection.Draw); + + if (drawTerrain) + { + SetRasterizerState(); + TerrainBatch.DrawWithZFiltering(IsInCurrentZLevel); + } + + if (drawEnvCells) + DrawBufferWithZSlicing(RB_EnvCell, true); + + if (drawStaticObjs) + DrawBufferWithZSlicing(RB_StaticObjs); + + if (drawBuildings) + DrawBufferWithZSlicing(RB_Buildings); + + if (drawScenery) + DrawBufferWithZSlicing(RB_Scenery); + + if (drawInstances && Server.InstancesLoaded) + DrawBufferWithZSlicing(RB_Instances); + + if (drawEncounters && Server.EncountersLoaded) + DrawBufferWithZSlicing(RB_Encounters); + + DrawBufferWithZSlicing(RB_Animated); + + if (Picker.HitVertices != null) + Picker.DrawHitPoly(); + + PerfTimer.Stop(ProfilerSection.Draw); + } + + private void DrawBufferWithZSlicing(Dictionary batches) + { + SetRasterizerState(CullMode.None); + + foreach (var batch in batches.Values) + batch.DrawFiltered(IsInCurrentZLevel); + } + + private void DrawBufferWithZSlicing(Dictionary batches) + { + SetRasterizerState(CullMode.None); + + foreach (var batch in batches.Values) + batch.DrawFiltered(IsInCurrentZLevel); + } + + private void DrawBufferWithZSlicing(Dictionary batches, bool culling = false) + { + var cullMode = WorldViewer.Instance.DungeonMode || culling ? + CullMode.CullClockwiseFace : CullMode.None; + + SetRasterizerState(cullMode); + + Effect.CurrentTechnique = Effect.Techniques["TexturedInstanceEnv"]; + Effect_Clamp.CurrentTechnique = Effect_Clamp.Techniques["TexturedInstanceEnv"]; + + foreach (var batch in batches.Values) + { + batch.DrawFiltered(IsInCurrentZLevel); + } + } + public void ClearBuffer() { diff --git a/ACViewer/Render/Camera.cs b/ACViewer/Render/Camera.cs index e758253..5148b71 100644 --- a/ACViewer/Render/Camera.cs +++ b/ACViewer/Render/Camera.cs @@ -10,10 +10,13 @@ using ACE.Server.Physics.Common; using ACViewer.Config; +using ACViewer.Entity; using ACViewer.Enum; using ACViewer.Extensions; using ACViewer.Render; using ACViewer.View; +using GameTime = Microsoft.Xna.Framework.GameTime; +using Position = ACE.Server.Physics.Common.Position; namespace ACViewer { @@ -275,6 +278,12 @@ public Matrix CreateProjection() public bool Locked { get; set; } public System.Windows.Point LastSetPoint { get; set; } + + private bool WasBindingActive(GameKeyBinding binding) + { + if (LastKeyboardState == null) return false; + return binding.Matches(LastKeyboardState, GameView.InputManager.GetCurrentModifiers()); + } public void Update(GameTime gameTime) { @@ -285,16 +294,46 @@ public void Update(GameTime gameTime) if (!GameView.IsActive) return; - if (keyboardState.IsKeyDown(Keys.W)) + // Get key bindings + var config = ConfigManager.Config.KeyBindingConfig; + + // Movement controls + if (GameView.InputManager.IsBindingActive(config.MoveForward)) Position += Dir * Speed; - if (keyboardState.IsKeyDown(Keys.S)) + if (GameView.InputManager.IsBindingActive(config.MoveBackward)) Position -= Dir * Speed; - if (keyboardState.IsKeyDown(Keys.A)) + if (GameView.InputManager.IsBindingActive(config.StrafeLeft)) Position += Vector3.Cross(Up, Dir) * Speed; - if (keyboardState.IsKeyDown(Keys.D)) + if (GameView.InputManager.IsBindingActive(config.StrafeRight)) Position -= Vector3.Cross(Up, Dir) * Speed; - if (keyboardState.IsKeyDown(Keys.Space)) + if (GameView.InputManager.IsBindingActive(config.MoveUp)) Position += Up * Speed; + if (GameView.InputManager.IsBindingActive(config.MoveDown)) + Position -= Up * Speed; + + // Z-level controls + if (GameView.InputManager.IsBindingActive(config.ToggleZLevel) && !WasBindingActive(config.ToggleZLevel)) + { + ConfigManager.Config.MapViewer.EnableZSlicing = !ConfigManager.Config.MapViewer.EnableZSlicing; + ConfigManager.Config.MapViewer.CurrentZLevel = 1; + } + + // Z-level adjustment + if (ConfigManager.Config.MapViewer.EnableZSlicing) + { + if (GameView.InputManager.IsBindingActive(config.IncreaseZLevel) && !WasBindingActive(config.IncreaseZLevel)) + { + var mapConfig = ConfigManager.Config.MapViewer; + mapConfig.CurrentZLevel = Math.Min(mapConfig.CurrentZLevel + 1, 20); + } + if (GameView.InputManager.IsBindingActive(config.DecreaseZLevel) && !WasBindingActive(config.DecreaseZLevel)) + { + var mapConfig = ConfigManager.Config.MapViewer; + mapConfig.CurrentZLevel--; + } + } + + LastKeyboardState = keyboardState; // camera speed control if (mouseState.ScrollWheelValue != PrevMouseState.ScrollWheelValue) @@ -368,6 +407,7 @@ public void Update(GameTime gameTime) //Console.WriteLine("Camera dir: " + GameView.Instance.Render.Camera.Dir); } + private KeyboardState LastKeyboardState; public int centerX => GameView.GraphicsDevice.Viewport.Width / 2; public int centerY => GameView.GraphicsDevice.Viewport.Height / 2; diff --git a/ACViewer/Render/GfxObjInstance_Shared.cs b/ACViewer/Render/GfxObjInstance_Shared.cs index da16e5e..175a00e 100644 --- a/ACViewer/Render/GfxObjInstance_Shared.cs +++ b/ACViewer/Render/GfxObjInstance_Shared.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; - +using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -25,9 +26,8 @@ public class GfxObjInstance_Shared public Dictionary BaseFormats_Alpha { get; set; } public List Vertices { get; set; } - + public List Instances { get; set; } - public VertexInstance[] Instances_ { get; set; } public VertexBuffer Shared_VB { get; set; } @@ -39,9 +39,7 @@ public class GfxObjInstance_Shared public GfxObjInstance_Shared(GfxObj gfxObj, Dictionary textureAtlasChains, Dictionary textureChanges = null, PaletteChanges paletteChanges = null) { GfxObj = gfxObj; - BuildStatic(gfxObj, textureAtlasChains, textureChanges, paletteChanges); - Instances = new List(); } @@ -185,6 +183,51 @@ public void Draw() foreach (var baseFormat in BaseFormats_Alpha.Values) baseFormat.Draw(Instances.Count); } + + public void DrawFiltered(Func filter) + { + if (Bindings == null) return; + + if (isDirty) + { + Instances_VB.SetData(Instances_); + isDirty = false; + } + + // Store original instances + var originalInstances = Instances_.ToArray(); + + // Filter instances + var filteredInstances = Instances.Where(instance => filter(instance.Position)).ToArray(); + + if (filteredInstances.Length > 0) + { + // Update vertex buffer with filtered instances + Instances_ = filteredInstances; + Instances_VB.SetData(filteredInstances); + + GraphicsDevice.SetVertexBuffers(Bindings); + + Effect.CurrentTechnique = Effect.Techniques["TexturedInstance"]; + Effect_Clamp.CurrentTechnique = Effect_Clamp.Techniques["TexturedInstance"]; + + foreach (var baseFormat in BaseFormats_Solid.Values) + baseFormat.Draw(filteredInstances.Length); + + if (Buffer.drawAlpha) + { + Effect.CurrentTechnique = Effect.Techniques["TexturedInstanceAlpha"]; + Effect_Clamp.CurrentTechnique = Effect_Clamp.Techniques["TexturedInstanceAlpha"]; + } + + foreach (var baseFormat in BaseFormats_Alpha.Values) + baseFormat.Draw(filteredInstances.Length); + } + + // Restore original instances + Instances_ = originalInstances; + Instances_VB.SetData(originalInstances); + } public void Dispose() { diff --git a/ACViewer/Render/InstanceBatch.cs b/ACViewer/Render/InstanceBatch.cs index 10c413b..b7d363c 100644 --- a/ACViewer/Render/InstanceBatch.cs +++ b/ACViewer/Render/InstanceBatch.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; - +using System; +using System.Collections.Generic; +using System.Linq; using ACE.Entity.Enum; - +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace ACViewer.Render @@ -17,6 +18,28 @@ public class InstanceBatch public VertexBuffer InstanceBuffer { get; set; } public R_Environment R_Environment { get; set; } + + public void DrawFiltered(Func filter) + { + // Store original instances + var originalInstances = new List(Instances_Env); + + // Filter instances based on Z position + Instances_Env = Instances_Env.Where(instance => filter(instance.Position)).ToList(); + + if (Instances_Env.Count > 0) + { + // Rebuild instance buffer with filtered instances + BuildInstanceBuffer(); + BuildBindings(); + Draw(); + } + + // Restore original instances + Instances_Env = originalInstances; + BuildInstanceBuffer(); + BuildBindings(); + } public InstanceBatch(R_EnvCell envCell) { diff --git a/ACViewer/Render/Render.cs b/ACViewer/Render/Render.cs index 110a17a..56d44d2 100644 --- a/ACViewer/Render/Render.cs +++ b/ACViewer/Render/Render.cs @@ -25,6 +25,9 @@ public class Render // multiple SamplerStates in the same .fx file apparently don't work public static Effect Effect_Clamp { get; set; } + + // Add to existing properties + private MapViewerOptions Config => ConfigManager.Config.MapViewer; public Camera Camera { @@ -67,21 +70,23 @@ public void SetRasterizerState(bool wireframe = true) GraphicsDevice.RasterizerState = rs; } - + public void Draw() { GraphicsDevice.Clear(ConfigManager.Config.BackgroundColors.WorldViewer); SetRasterizerState(false); - + Effect.Parameters["xView"].SetValue(Camera.ViewMatrix); Effect_Clamp.Parameters["xView"].SetValue(Camera.ViewMatrix); - //landblock.Draw(); - Buffer.Draw(); + if (ConfigManager.Config.MapViewer.EnableZSlicing) + Buffer.DrawWithZSlicing(); + else + Buffer.Draw(); - //DrawEmitters_Naive(); DrawEmitters_Batch(); + DrawHUD(); } public bool ParticlesInitted { get; set; } @@ -186,14 +191,29 @@ public void DestroyEmitters() private static readonly Vector2 TextPos = new Vector2(10, 10); + // DrawHUD to show Z-slice information public void DrawHUD() { - var cameraPos = GameView.Camera.GetPosition(); + var text = ""; + + if (ConfigManager.Config.MapViewer.EnableZSlicing) + { + var config = ConfigManager.Config.MapViewer; + string levelPrefix = config.CurrentZLevel < 0 ? "B" : ""; // Add "B" prefix for basement levels + int displayLevel = config.CurrentZLevel < 0 ? -config.CurrentZLevel : config.CurrentZLevel; + + text += $"Current Z-Level: {levelPrefix}{displayLevel}\n"; // Shows B1, B2, etc. for basement levels + text += $"Height Range: {(config.CurrentZLevel - 1) * config.LevelHeight:F1}m - {config.CurrentZLevel * config.LevelHeight:F1}m\n"; + } + var cameraPos = Camera.GetPosition(); if (cameraPos != null) + text += $"Location: {cameraPos}"; + + if (!string.IsNullOrEmpty(text)) { SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.LinearClamp); - SpriteBatch.DrawString(Font, $"Location: {cameraPos}", TextPos, Color.White); + SpriteBatch.DrawString(Font, text, TextPos, Color.White); SpriteBatch.End(); } } diff --git a/ACViewer/Render/TerrainBatch.cs b/ACViewer/Render/TerrainBatch.cs index 5615084..7c6b764 100644 --- a/ACViewer/Render/TerrainBatch.cs +++ b/ACViewer/Render/TerrainBatch.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; - +using System.Linq; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace ACViewer.Render @@ -56,6 +57,33 @@ public void Draw() foreach (var batch in Batches) batch.Draw(); } + + public void DrawWithZFiltering(Func filter) + { + Effect.CurrentTechnique = Effect.Techniques["LandscapeSinglePass"]; + + if (OverlayAtlasChain.TextureAtlases.Count > 0) + Effect.Parameters["xOverlays"].SetValue(OverlayAtlasChain.TextureAtlases[0]._Textures); + if (AlphaAtlasChain.TextureAtlases.Count > 0) + Effect.Parameters["xAlphas"].SetValue(AlphaAtlasChain.TextureAtlases[0]._Textures); + + foreach (var batch in Batches) + { + if (batch.Vertices.Count == 0) continue; + + var originalVertices = new List(batch.Vertices); + batch.Vertices = batch.Vertices.Where(v => filter(v.Position)).ToList(); + + if (batch.Vertices.Count > 0) + { + batch.OnCompleted(); + batch.Draw(); + } + + batch.Vertices = originalVertices; + batch.OnCompleted(); + } + } public void Dispose() { diff --git a/ACViewer/View/KeyBindingDialog.xaml b/ACViewer/View/KeyBindingDialog.xaml new file mode 100644 index 0000000..34b21e4 --- /dev/null +++ b/ACViewer/View/KeyBindingDialog.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ACViewer/View/KeyBindingDialog.xaml.cs b/ACViewer/View/KeyBindingDialog.xaml.cs new file mode 100644 index 0000000..097b6a5 --- /dev/null +++ b/ACViewer/View/KeyBindingDialog.xaml.cs @@ -0,0 +1,145 @@ +using System; +using System.ComponentModel; +using System.Windows.Input; +using System.Windows; +using System.Windows.Threading; +using Microsoft.Xna.Framework.Input; +using ACViewer.Entity; +using ACViewer.Input; +using MessageBox = System.Windows.MessageBox; + +namespace ACViewer.View +{ + public partial class KeyBindingDialog : Window + { + private readonly DispatcherTimer _timer; + private int _remainingTime; + private const int TIMEOUT_SECONDS = 5; + private readonly KeyRecorder _recorder; + private bool _isClosing; + + public GameKeyBinding ResultBinding { get; private set; } + + public KeyBindingDialog() + { + InitializeComponent(); + + _remainingTime = TIMEOUT_SECONDS; + _timer = new DispatcherTimer + { + Interval = TimeSpan.FromSeconds(1) + }; + _timer.Tick += Timer_Tick; + + _recorder = new KeyRecorder(); + _recorder.OnKeyStateChanged += Recorder_KeyStateChanged; + _recorder.RecordingComplete += Recorder_RecordingComplete; + _recorder.RecordingCancelled += Recorder_RecordingCancelled; + _recorder.RecordingError += Recorder_RecordingError; + + Loaded += KeyBindingDialog_Loaded; + Closing += KeyBindingDialog_Closing; + } + + private void KeyBindingDialog_Loaded(object sender, RoutedEventArgs e) + { + UpdateTimeoutText(); + _timer.Start(); + _recorder.StartRecording(this); + } + + private void KeyBindingDialog_Closing(object sender, CancelEventArgs e) + { + _isClosing = true; + _timer.Stop(); + _recorder.StopRecording(); + } + + private void Recorder_KeyStateChanged(Keys mainKey, ModifierKeys modifiers) + { + Dispatcher.BeginInvoke(new Action(() => + { + string displayText; + if (mainKey == Keys.None) + { + if (modifiers != ModifierKeys.None) + displayText = $"{modifiers}+"; + else + displayText = $"Waiting for input... ({_remainingTime}s)"; + } + else + { + var modifierStr = modifiers != ModifierKeys.None ? $"{modifiers}+" : ""; + displayText = $"{modifierStr}{mainKey}"; + } + + txtCurrentKeys.Text = displayText; + })); + } + + private void Timer_Tick(object sender, EventArgs e) + { + _remainingTime--; + UpdateTimeoutText(); + + if (_remainingTime <= 0) + { + _timer.Stop(); + SafeClose(false); + } + } + + private void UpdateTimeoutText() + { + Dispatcher.BeginInvoke(new Action(() => + { + txtCurrentKeys.Text = $"Waiting for input... ({_remainingTime}s)"; + })); + } + + private void SafeClose(bool? dialogResult) + { + if (!_isClosing) + { + _isClosing = true; + DialogResult = dialogResult; + Close(); + } + } + + private void Recorder_RecordingComplete(GameKeyBinding binding) + { + Dispatcher.BeginInvoke(new Action(() => + { + if (!_isClosing) + { + ResultBinding = binding; + SafeClose(true); + } + })); + } + + private void Recorder_RecordingCancelled() + { + Dispatcher.BeginInvoke(new Action(() => + { + if (!_isClosing) + { + SafeClose(false); + } + })); + } + + private void Recorder_RecordingError(string error) + { + Dispatcher.BeginInvoke(new Action(() => + { + if (!_isClosing) + { + MessageBox.Show(this, error, "Error", MessageBoxButton.OK, MessageBoxImage.Error); + SafeClose(false); + } + })); + } + } +} \ No newline at end of file diff --git a/ACViewer/View/KeyboardConfig.xaml b/ACViewer/View/KeyboardConfig.xaml new file mode 100644 index 0000000..0948c12 --- /dev/null +++ b/ACViewer/View/KeyboardConfig.xaml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +