diff --git a/3074.csproj b/3074.csproj
index 2d9d91f..40e44b5 100644
--- a/3074.csproj
+++ b/3074.csproj
@@ -72,15 +72,21 @@
App.xaml
Code
-
-
-
-
-
-
-
+
+
+
+
+ ConfigWindow.xaml
+
+
+
+ KeybindingDialog.xaml
+
+
OverlayWindow.xaml
+
+
Code
@@ -92,9 +98,13 @@
+
+
-
+
+
+
\ No newline at end of file
diff --git a/App.xaml b/App.xaml
index efbba5f..d6be09d 100644
--- a/App.xaml
+++ b/App.xaml
@@ -1,7 +1,7 @@
-
diff --git a/App.xaml.cs b/App.xaml.cs
index d278f35..83271f2 100644
--- a/App.xaml.cs
+++ b/App.xaml.cs
@@ -3,8 +3,11 @@ using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Threading;
+using tsf_3074.Data;
+using tsf_3074.Window;
+using tsf_3074.Windows.Dialog;
-namespace _3074
+namespace tsf_3074
{
///
/// Interaction logic for App.xaml
@@ -14,6 +17,7 @@ namespace _3074
private void App_OnStartup(object sender, StartupEventArgs e)
{
SetCurrentProcessExplicitAppUserModelID("com.squirrel.Tsf");
+ Config.Instance.ToString();
Tsf.FFullGame.ToString();
var window = new OverlayWindow();
window.Show();
diff --git a/Config.cs b/Config.cs
deleted file mode 100644
index d0622c4..0000000
--- a/Config.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using Newtonsoft.Json;
-
-namespace _3074
-{
- public static class Config
- {
- public static ConfigData Instance => _instance ?? ReadConfig();
- private static ConfigData _instance;
-
- private static ConfigData ReadConfig(string path = "config.json")
- {
- if (File.Exists(path))
- {
- return _instance = JsonConvert.DeserializeObject(File.ReadAllText(path));
- }
-
- var defaults = new ConfigData
- {
- Destiny2Path = "Your Destiny 2 application path"
- };
-
- File.WriteAllText(path, JsonConvert.SerializeObject(defaults));
-
- throw new Exception("Config is not set");
- }
- }
-
- public class ConfigData
- {
- public string Destiny2Path;
- }
-
-}
\ No newline at end of file
diff --git a/Data/Config.cs b/Data/Config.cs
new file mode 100644
index 0000000..1a29a44
--- /dev/null
+++ b/Data/Config.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Windows;
+using Microsoft.Win32;
+using Newtonsoft.Json;
+
+namespace tsf_3074.Data
+{
+ public static class Config
+ {
+ public static ConfigData Instance => _instance ?? ReadConfig();
+ private static ConfigData _instance;
+
+ private static ConfigData ReadConfig(string path = "config.json")
+ {
+ if (File.Exists(path))
+ {
+ return _instance = JsonConvert.DeserializeObject(File.ReadAllText(path));
+ }
+
+ FindDestiny2(out var appPath);
+ if (appPath == "")
+ {
+ MessageBox.Show("You'd better to read the README document.");
+ Application.Current.Shutdown();
+ throw new Exception();
+ }
+
+ _instance = new ConfigData
+ {
+ Destiny2Path = appPath,
+ Hotkeys = new Dictionary
+ {
+ { "tsf_3074", new HotkeyPair { vk = VirtualKey.VkG, fs = VirtualKey.ModCtrl } },
+ { "tsf_3074_ul", new HotkeyPair { vk = VirtualKey.VkH, fs = VirtualKey.ModCtrl } },
+ { "tsf_7500", new HotkeyPair { vk = VirtualKey.VkT, fs = VirtualKey.ModCtrl } },
+ { "tsf_fg", new HotkeyPair { vk = VirtualKey.VkJ, fs = VirtualKey.ModCtrl } },
+ { "tsf_27k", new HotkeyPair { vk = VirtualKey.VkN, fs = VirtualKey.ModAlt } },
+ { "tsf_30k", new HotkeyPair { vk = VirtualKey.VkB, fs = VirtualKey.ModAlt } },
+ }
+ };
+
+ File.WriteAllText(path, JsonConvert.SerializeObject(_instance));
+
+ return _instance;
+ }
+
+ public static void SaveConfig(string path = "config.json")
+ {
+ File.WriteAllText(path, JsonConvert.SerializeObject(_instance));
+ }
+
+ private static void FindDestiny2(out string appPath)
+ {
+ var dialog = new OpenFileDialog
+ {
+ Multiselect = false,
+ Title = "Select Destiny 2 Application",
+ Filter = "Destiny 2|destiny2.exe"
+ };
+ dialog.ShowDialog();
+ Console.WriteLine(dialog.FileName);
+ appPath = dialog.FileName;
+ }
+ }
+
+ public class ConfigData
+ {
+ public string Destiny2Path;
+ public Dictionary Hotkeys;
+ }
+
+ public class HotkeyPair
+ {
+ public uint vk;
+ public uint fs;
+ }
+}
\ No newline at end of file
diff --git a/OverlayWindow.xaml.cs b/OverlayWindow.xaml.cs
deleted file mode 100644
index 0acb61e..0000000
--- a/OverlayWindow.xaml.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-using System;
-using System.ComponentModel;
-using System.Globalization;
-using System.Windows;
-using System.Windows.Data;
-using System.Windows.Media;
-
-namespace _3074
-{
- public partial class OverlayWindow : Window
- {
- internal readonly HotkeyManager _hotkey;
-
- public OverlayWindow()
- {
- InitializeComponent();
- DataContext = new OverlayViewModel();
- _hotkey = new HotkeyManager(this);
- }
-
- protected override void OnSourceInitialized(EventArgs e)
- {
- base.OnSourceInitialized(e);
- TsfFilter.AllFilters.ForEach(f => f.RegisterHotkey(_hotkey));
- _hotkey.OnSourceInitialized();
- }
-
- protected override void OnClosed(EventArgs e)
- {
- _hotkey.OnClosed();
- base.OnClosed(e);
- }
- }
-
- public class OverlayViewModel : INotifyPropertyChanged
- {
- public event PropertyChangedEventHandler PropertyChanged;
-
- public string F3074 => Tsf.F3074.GetState();
- public string F3074UL => Tsf.F3074UL.GetState();
- public string F7500 => Tsf.F7500.GetState();
- public string F27K => Tsf.F27K.GetState();
- public string F30K => Tsf.F30K.GetState();
- public string FFullGame => Tsf.FFullGame.GetState();
-
- public OverlayViewModel()
- {
- Tsf.F3074.OnStateChanged += () => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(F3074)));
- Tsf.F3074UL.OnStateChanged += () => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(F3074UL)));
- Tsf.F7500.OnStateChanged += () => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(F7500)));
- Tsf.F27K.OnStateChanged += () => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(F27K)));
- Tsf.F30K.OnStateChanged += () => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(F30K)));
- Tsf.FFullGame.OnStateChanged += () => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FFullGame)));
- }
- }
-
- public class OnOrOffColorSelector : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- if (!(value is string str)) return Colors.Orange;
- switch (str.ToLower())
- {
- case "on":
- return Colors.GreenYellow;
- case "off":
- return Colors.Red;
- default:
- return Colors.BlueViolet;
- }
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new Exception();
- }
- }
-}
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e756854
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+# Thirty Seventy Four (3074)
diff --git a/Tsf.cs b/Tsf.cs
index 30ccc6d..992fa2e 100644
--- a/Tsf.cs
+++ b/Tsf.cs
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Windows.Input;
using NetLimiter.Service;
+using tsf_3074.Data;
-namespace _3074
+namespace tsf_3074
{
public static class Tsf
{
@@ -28,12 +30,23 @@ namespace _3074
uint fs = 0, uint vk = 0)
{
var cli = GetClient();
- var name = GetFilterNameFor(portName);
+ var name = GetFilterNameFor(portName, upload);
var filter = cli.Filters.FirstOrDefault(f => f.Name == name) ??
cli.AddFilter(new Filter(name)
{
- Functions = { new FFRemotePortInRange(new PortRangeFilterValue(portStart, portEnd)) }
+ Functions =
+ {
+ new FFPathEqual(Config.Instance.Destiny2Path),
+ new FFRemotePortInRange(new PortRangeFilterValue(portStart, portEnd))
+ }
});
+ var hotkey = Config.Instance.Hotkeys[name];
+ if (hotkey != null)
+ {
+ vk = hotkey.vk;
+ fs = hotkey.fs;
+ }
+
return new TsfFilter(name, filter, cli, upload, 1, fs, vk);
}
@@ -46,6 +59,13 @@ namespace _3074
{
Functions = { new FFPathEqual(Config.Instance.Destiny2Path) }
});
+ var hotkey = Config.Instance.Hotkeys[name];
+ if (hotkey != null)
+ {
+ vk = hotkey.vk;
+ fs = hotkey.fs;
+ }
+
return new TsfFilter(name, filter, cli, false, 811, fs, vk);
}
@@ -74,11 +94,20 @@ namespace _3074
private readonly NLClient _client;
private readonly bool _upload;
private readonly uint _limitSize; // the limit rate, normally is 1, only FG is 800 or 811
- private readonly uint _fs;
- private readonly uint _vk;
+
+ private uint _fs;
+ private uint _vk;
+
+ public string Name => _name;
+
+ public uint FS => _fs;
+ public uint VK => _vk;
private Rule _rule;
+ /**
+ * Invoked every time the IsEnable is changed. (Maybe not actually changed, like from false to false.)
+ */
public event OnEvent OnStateChanged = delegate { };
public static readonly List AllFilters = new List();
@@ -94,32 +123,48 @@ namespace _3074
_fs = fs;
_vk = vk;
- GetLimitRule();
+ InitLimitRule();
AllFilters.Add(this);
}
- public void RegisterHotkey(HotkeyManager manager)
+ ~TsfFilter()
+ {
+ AllFilters.Remove(this);
+ }
+
+ /**
+ * Set the keybindings. Need to reload the hotkey manager manually!
+ */
+ public void SetHotkey(uint vk, uint fs)
+ {
+ _vk = vk;
+ _fs = fs;
+ }
+
+ /**
+ * Register the hotkey to the HotkeyManager.
+ */
+ internal void RegisterHotkey(HotkeyManager manager)
{
if (_vk != 0)
{
- Console.WriteLine($"Registering Hotkey for {_name}");
+ Console.WriteLine(
+ $"Registering Hotkey for {_name} {VirtualKey.GetModifierName(_fs)} + {KeyInterop.KeyFromVirtualKey((int)_vk)}");
manager.RegisterHotkey(_fs, _vk, () =>
{
Toggle();
- var currState = GetLimitRule().IsEnabled ? "On" : "Off";
- Console.WriteLine($"Updated [{_name}] {currState}");
+ Console.WriteLine($"Updated {_name} to {GetState()}");
});
}
}
- private Rule GetLimitRule()
+ private void InitLimitRule()
{
- if (_rule != null) return _rule;
+ if (_rule != null) return;
var dir = _upload ? RuleDir.Out : RuleDir.In;
_rule = _client.Rules.FirstOrDefault(r => r.FilterId == _filter.Id && r.Dir == dir) ??
_client.AddRule(_filter.Id, new LimitRule(dir, _limitSize) { IsEnabled = false });
- return _rule;
}
public void Toggle()
diff --git a/OnEvent.cs b/Utils/OnEvent.cs
similarity index 63%
rename from OnEvent.cs
rename to Utils/OnEvent.cs
index d7735a1..83468e3 100644
--- a/OnEvent.cs
+++ b/Utils/OnEvent.cs
@@ -1,4 +1,4 @@
-namespace _3074
+namespace tsf_3074
{
public delegate void OnEvent();
}
\ No newline at end of file
diff --git a/VirtualKey.cs b/Utils/VirtualKey.cs
similarity index 95%
rename from VirtualKey.cs
rename to Utils/VirtualKey.cs
index dc64ea2..e48daf8 100644
--- a/VirtualKey.cs
+++ b/Utils/VirtualKey.cs
@@ -1,4 +1,4 @@
-namespace _3074
+namespace tsf_3074
{
public static class VirtualKey
{
@@ -8,6 +8,22 @@
public const uint ModShift = 0x0004;
public const uint ModWin = 0x0008;
+ public static string GetModifierName(uint mod)
+ {
+ switch (mod)
+ {
+ case ModNone: return "None";
+ case ModAlt: return "Alt";
+ case ModCtrl: return "Ctrl";
+ case ModShift: return "Shift";
+ case ModCtrl | ModAlt: return "Ctrl + Alt";
+ case ModCtrl | ModShift: return "Ctrl + Shift";
+ case ModAlt | ModShift: return "Alt + Shift";
+ case ModCtrl | ModAlt | ModShift: return "Ctrl + Shift + Alt";
+ default: return "Unknown";
+ }
+ }
+
// https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
public const uint VkLButton = 0x01; // Left mouse button
diff --git a/Windows/ConfigWindow.xaml b/Windows/ConfigWindow.xaml
new file mode 100644
index 0000000..01149f3
--- /dev/null
+++ b/Windows/ConfigWindow.xaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Windows/ConfigWindow.xaml.cs b/Windows/ConfigWindow.xaml.cs
new file mode 100644
index 0000000..b3082d1
--- /dev/null
+++ b/Windows/ConfigWindow.xaml.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Windows;
+using System.Windows.Input;
+using tsf_3074.Windows.Dialog;
+
+namespace tsf_3074.Windows
+{
+ public partial class ConfigWindow : System.Windows.Window
+ {
+ // dont use this ctor,
+ // use #ShowConfigDialog instead!
+ public ConfigWindow()
+ {
+ InitializeComponent();
+ }
+
+ private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
+ {
+ if (!(e.Source is System.Windows.Controls.Button button)) return;
+
+ switch (button.Content)
+ {
+ case "3074":
+ SetHotkeyFor(Tsf.F3074);
+ break;
+ case "3074 UL":
+ SetHotkeyFor(Tsf.F3074UL);
+ break;
+ case "7500":
+ SetHotkeyFor(Tsf.F7500);
+ break;
+ case "27k":
+ SetHotkeyFor(Tsf.F27K);
+ break;
+ case "30k":
+ SetHotkeyFor(Tsf.F30K);
+ break;
+ case "FG":
+ SetHotkeyFor(Tsf.FFullGame);
+ break;
+ }
+ }
+
+ /**
+ * Use KeybindingDialog to receive a keybinding,
+ * and set the value to TsfFilter.
+ * When the Config window exits, the hotkeys should be reloaded.
+ */
+ private static void SetHotkeyFor(TsfFilter filter)
+ {
+ KeybindingDialog.OpenKeybindingDialog(out var key, out var fs,
+ KeyInterop.KeyFromVirtualKey((int)filter.VK), filter.FS);
+ // don't change if escape is pressed
+ if (key == Key.Escape) return;
+ filter.SetHotkey((uint)KeyInterop.VirtualKeyFromKey(key), fs);
+ }
+
+ private static bool _showing;
+
+ /**
+ * Use this static method to ensure there is only 1 instance of Config window.
+ */
+ public static void ShowConfigDialog()
+ {
+ if (_showing) return;
+ _showing = true;
+
+ var window = new ConfigWindow();
+ window.Closed += (sender, e) => { _showing = false; };
+
+ window.ShowDialog();
+ }
+ }
+}
\ No newline at end of file
diff --git a/HotkeyManager.cs b/Windows/Control/HotkeyManager.cs
similarity index 77%
rename from HotkeyManager.cs
rename to Windows/Control/HotkeyManager.cs
index 9fc6fe4..71d6c9e 100644
--- a/HotkeyManager.cs
+++ b/Windows/Control/HotkeyManager.cs
@@ -1,16 +1,15 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
-using System.Windows;
using System.Windows.Interop;
-namespace _3074
+namespace tsf_3074
{
public partial class HotkeyManager
{
- private readonly Window _window;
+ private readonly System.Windows.Window _window;
- public HotkeyManager(Window window)
+ public HotkeyManager(System.Windows.Window window)
{
_window = window;
}
@@ -22,6 +21,10 @@ namespace _3074
private const int HotkeyIdStart = 9000;
private int _hotkeyId = HotkeyIdStart;
+ /**
+ * Register a hotkey with given modifiers and virtual keys.
+ * When the keybinding is pressed, the onEvent is invoked.
+ */
public void RegisterHotkey(uint fsModifiers, uint vk, OnEvent onEvent)
{
var thisId = _hotkeyId++;
@@ -29,7 +32,7 @@ namespace _3074
_listener[thisId] = onEvent;
}
- private void UnregisterAllHotkeys()
+ public void UnregisterAllHotkeys()
{
var helper = new WindowInteropHelper(_window);
while (_hotkeyId >= HotkeyIdStart)
@@ -38,12 +41,18 @@ namespace _3074
}
}
+ /**
+ * Add the hook to the Overlay window
+ */
public void OnSourceInitialized()
{
_source = HwndSource.FromHwnd(new WindowInteropHelper(_window).Handle) ?? throw new Exception();
_source.AddHook(HwndHook);
}
+ /**
+ * Remove the hook from Overlay window
+ */
public void OnClosed()
{
_source.RemoveHook(HwndHook);
@@ -51,6 +60,10 @@ namespace _3074
UnregisterAllHotkeys();
}
+ /**
+ * The callback for keybinding being pressed.
+ * Don't change this function, use registerHotkey!
+ */
private IntPtr HwndHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg != 0x0312) return IntPtr.Zero;
diff --git a/Windows/Dialog/KeybindingDialog.xaml b/Windows/Dialog/KeybindingDialog.xaml
new file mode 100644
index 0000000..eed8c33
--- /dev/null
+++ b/Windows/Dialog/KeybindingDialog.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/Windows/Dialog/KeybindingDialog.xaml.cs b/Windows/Dialog/KeybindingDialog.xaml.cs
new file mode 100644
index 0000000..368de5b
--- /dev/null
+++ b/Windows/Dialog/KeybindingDialog.xaml.cs
@@ -0,0 +1,84 @@
+using System.ComponentModel;
+using System.Windows.Input;
+
+namespace tsf_3074.Windows.Dialog
+{
+ public partial class KeybindingDialog : System.Windows.Window
+ {
+ private readonly KeybindingDialogView _data = new KeybindingDialogView();
+
+ public KeybindingDialog()
+ {
+ InitializeComponent();
+ DataContext = _data;
+ }
+
+ private void KeybindingDialog_OnKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Return || e.Key == Key.Escape)
+ {
+ Close();
+ return;
+ }
+
+ var ctrl = Keyboard.IsKeyDown(Key.LeftCtrl);
+ var alt = Keyboard.IsKeyDown(Key.LeftAlt);
+ var shift = Keyboard.IsKeyDown(Key.LeftShift);
+
+ var fs = ctrl ? VirtualKey.ModCtrl :
+ alt ? VirtualKey.ModAlt :
+ shift ? VirtualKey.ModShift : 0;
+
+ _data.SetKey(e.Key, fs);
+ }
+
+ public static void OpenKeybindingDialog(out Key key, out uint fs, Key initKey = Key.None, uint initFs = 0)
+ {
+ var dialog = new KeybindingDialog
+ {
+ _data =
+ {
+ Key = initKey,
+ Modifier = initFs
+ }
+ };
+
+ // wait for exit, and put the values to the ref fields
+ dialog.ShowDialog();
+
+ key = dialog._data.Key;
+ fs = dialog._data.Modifier;
+ }
+ }
+
+ public class KeybindingDialogView : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public string StatusText { get; set; } = "Press keys";
+
+ internal Key Key { get; set; }
+ internal uint Modifier { get; set; }
+
+ public void SetKey(Key key, uint fsMod)
+ {
+ if (key == Key.LeftCtrl || key == Key.LeftAlt || key == Key.LeftShift || key == Key.System) return;
+
+ Key = key;
+ Modifier = fsMod;
+ if (fsMod != 0)
+ {
+ var mod = fsMod == VirtualKey.ModCtrl ? "Ctrl" :
+ fsMod == VirtualKey.ModAlt ? "Alt" :
+ fsMod == VirtualKey.ModShift ? "Shift" : "Unknown";
+ StatusText = $"{mod} + {Key}";
+ }
+ else
+ {
+ StatusText = $"{Key}";
+ }
+
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusText)));
+ }
+ }
+}
\ No newline at end of file
diff --git a/OverlayWindow.xaml b/Windows/OverlayWindow.xaml
similarity index 91%
rename from OverlayWindow.xaml
rename to Windows/OverlayWindow.xaml
index a1e20e4..dddc195 100644
--- a/OverlayWindow.xaml
+++ b/Windows/OverlayWindow.xaml
@@ -1,16 +1,16 @@
-
+ FontFamily="Consolas" d:DataContext="{d:DesignInstance window:OverlayViewModel}">
diff --git a/Windows/OverlayWindow.xaml.cs b/Windows/OverlayWindow.xaml.cs
new file mode 100644
index 0000000..a234535
--- /dev/null
+++ b/Windows/OverlayWindow.xaml.cs
@@ -0,0 +1,82 @@
+using System;
+using System.ComponentModel;
+using tsf_3074.Data;
+using tsf_3074.Windows;
+
+namespace tsf_3074.Window
+{
+ public partial class OverlayWindow : System.Windows.Window
+ {
+ private readonly HotkeyManager _hotkey;
+
+ public OverlayWindow()
+ {
+ InitializeComponent();
+ DataContext = new OverlayViewModel();
+ _hotkey = new HotkeyManager(this);
+ }
+
+ private void ReloadAllHotkeys(bool firstTime = false)
+ {
+ if (!firstTime) _hotkey.UnregisterAllHotkeys();
+
+ TsfFilter.AllFilters.ForEach(f => f.RegisterHotkey(_hotkey));
+
+ // add additional global keybindings (Alt+C)
+ _hotkey.RegisterHotkey(VirtualKey.ModAlt, VirtualKey.VkC, () =>
+ {
+ Console.WriteLine("Configuration hotkey is pressed!");
+ _hotkey.UnregisterAllHotkeys(); // unregister all hotkeys to prevent the issue, which the taken hotkeys are not able to be read from the dialog.
+ ConfigWindow.ShowConfigDialog();
+ Console.WriteLine("Configuration dialog exited.");
+ Console.WriteLine("Saving configuration!");
+ TsfFilter.AllFilters.ForEach(f =>
+ {
+ Config.Instance.Hotkeys[f.Name] = new HotkeyPair { vk = f.VK, fs = f.FS };
+ });
+ Config.SaveConfig();
+ Console.WriteLine("Refreshing hotkeys!");
+ ReloadAllHotkeys();
+ });
+ }
+
+ protected override void OnSourceInitialized(EventArgs e)
+ {
+ base.OnSourceInitialized(e);
+ ReloadAllHotkeys(true);
+ _hotkey.OnSourceInitialized();
+ }
+
+ protected override void OnClosed(EventArgs e)
+ {
+ _hotkey.OnClosed();
+ base.OnClosed(e);
+ }
+ }
+
+ public class OverlayViewModel : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public string F3074 => Tsf.F3074.GetState();
+ public string F3074UL => Tsf.F3074UL.GetState();
+ public string F7500 => Tsf.F7500.GetState();
+ public string F27K => Tsf.F27K.GetState();
+ public string F30K => Tsf.F30K.GetState();
+ public string FFullGame => Tsf.FFullGame.GetState();
+
+ public OverlayViewModel()
+ {
+ Tsf.F3074.OnStateChanged +=
+ () => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(F3074)));
+ Tsf.F3074UL.OnStateChanged +=
+ () => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(F3074UL)));
+ Tsf.F7500.OnStateChanged +=
+ () => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(F7500)));
+ Tsf.F27K.OnStateChanged += () => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(F27K)));
+ Tsf.F30K.OnStateChanged += () => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(F30K)));
+ Tsf.FFullGame.OnStateChanged += () =>
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FFullGame)));
+ }
+ }
+}
\ No newline at end of file
diff --git a/manual_zh.md b/manual_zh.md
new file mode 100644
index 0000000..87d63eb
--- /dev/null
+++ b/manual_zh.md
@@ -0,0 +1,24 @@
+# TSF 3074 使用手册
+
+## 安装
+
+1. 首先你需要安装 NetLimiter 5,你可以从任何其他地方下载到。
+2. 启动 TSF,会有一个文件选择弹窗,选择你的命运2文件(destiny2.exe)。
+3. 按下 Alt + C 打开快捷键设置窗口,你可以在这个窗口改绑按键。
+4. 例如点击 *3074 UL* 按钮,会弹出按键读取窗口,按下任意按键,例如 K,然后按回车,窗口会关闭。
+5. 所有按键改动完成后,右上角X掉,就会自动保存你的设置。
+6. 然后就可以进游戏用了。
+
+## FAQ
+
+### 开不起来?有问题?
+
+如果你遇到了疑难杂症,可以先看看目录下有没有以 `tsf-crash_` 开头的文件,把他发送给我。
+
+### 开源?
+
+为了避免被棒鸡检测特征,所以不公开分享源码,如果你希望为项目做贡献,可以联系我。
+
+### 怎么找到作者?
+
+你可以通过邮箱 r0yalist¥outlook.com 联系到我。