服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
390 lines
16 KiB
C#
390 lines
16 KiB
C#
namespace DNFLoadout;
|
|
|
|
internal sealed class TextPrompt : Form
|
|
{
|
|
private readonly TextBox _text = new() { Dock = DockStyle.Fill };
|
|
public string Value => _text.Text.Trim();
|
|
|
|
public TextPrompt(string title, string label, string initial = "")
|
|
{
|
|
Text = title;
|
|
Width = 360;
|
|
Height = 150;
|
|
StartPosition = FormStartPosition.CenterParent;
|
|
FormBorderStyle = FormBorderStyle.FixedDialog;
|
|
MaximizeBox = false;
|
|
MinimizeBox = false;
|
|
var panel = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(10), RowCount = 3 };
|
|
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 28));
|
|
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 36));
|
|
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
|
|
panel.Controls.Add(new Label { Text = label + "(按回车确认)", Dock = DockStyle.Fill }, 0, 0);
|
|
_text.Text = initial;
|
|
panel.Controls.Add(_text, 0, 1);
|
|
var ok = new Button { Text = "确定", DialogResult = DialogResult.OK, Dock = DockStyle.Right, Width = 90 };
|
|
var cancel = new Button { Text = "取消", DialogResult = DialogResult.Cancel, Dock = DockStyle.Right, Width = 90 };
|
|
var buttons = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.RightToLeft };
|
|
buttons.Controls.Add(ok);
|
|
buttons.Controls.Add(cancel);
|
|
panel.Controls.Add(buttons, 0, 2);
|
|
Controls.Add(panel);
|
|
AcceptButton = ok;
|
|
CancelButton = cancel;
|
|
}
|
|
}
|
|
|
|
internal sealed class HotkeyDialog : Form
|
|
{
|
|
private readonly TextBox _display = new() { ReadOnly = true, Dock = DockStyle.Top, Height = 28 };
|
|
private int _modifiers;
|
|
private int _keyCode;
|
|
|
|
public int Modifiers => _modifiers;
|
|
public int KeyCode => _keyCode;
|
|
|
|
public HotkeyDialog(int modifiers, int keyCode)
|
|
{
|
|
Text = "设置快捷键";
|
|
Width = 360;
|
|
Height = 190;
|
|
StartPosition = FormStartPosition.CenterParent;
|
|
FormBorderStyle = FormBorderStyle.FixedDialog;
|
|
MaximizeBox = false;
|
|
MinimizeBox = false;
|
|
_modifiers = modifiers;
|
|
_keyCode = keyCode;
|
|
_display.Text = Scheme.HotkeyTextOf(_modifiers, _keyCode);
|
|
_display.KeyDown += (_, e) =>
|
|
{
|
|
var (mods, code) = HotkeyManager.Capture(e.KeyData);
|
|
if (code == 0) return;
|
|
_modifiers = mods;
|
|
_keyCode = code;
|
|
_display.Text = Scheme.HotkeyTextOf(_modifiers, _keyCode);
|
|
e.SuppressKeyPress = true;
|
|
};
|
|
var hint = new Label
|
|
{
|
|
Text = "点击下面的输入框,然后按下想要的组合键。\n支持 F1~F12、字母、数字,可组合 Ctrl/Alt/Shift。",
|
|
Dock = DockStyle.Fill,
|
|
Padding = new Padding(8),
|
|
};
|
|
var ok = new Button { Text = "确定", DialogResult = DialogResult.OK, Dock = DockStyle.Right, Width = 90 };
|
|
var cancel = new Button { Text = "取消", DialogResult = DialogResult.Cancel, Dock = DockStyle.Right, Width = 90 };
|
|
var buttons = new FlowLayoutPanel { Dock = DockStyle.Bottom, Height = 40, FlowDirection = FlowDirection.RightToLeft };
|
|
buttons.Controls.Add(ok);
|
|
buttons.Controls.Add(cancel);
|
|
Controls.Add(hint);
|
|
Controls.Add(_display);
|
|
Controls.Add(buttons);
|
|
AcceptButton = ok;
|
|
CancelButton = cancel;
|
|
}
|
|
}
|
|
|
|
internal sealed class MainForm : Form
|
|
{
|
|
private readonly TextBox _server = new() { Width = 260 };
|
|
private readonly TextBox _token = new() { Width = 260, UseSystemPasswordChar = true };
|
|
private readonly ListBox _schemes = new() { Dock = DockStyle.Fill, IntegralHeight = false };
|
|
private readonly Label _status = new() { Dock = DockStyle.Bottom, Height = 64, TextAlign = ContentAlignment.MiddleLeft, Padding = new Padding(6) };
|
|
private readonly Button _add = new() { Text = "新增方案", Width = 100 };
|
|
private readonly Button _hotkey = new() { Text = "设置快捷键", Width = 100 };
|
|
private readonly Button _saveCurrent = new() { Text = "保存当前配装", Width = 120 };
|
|
private readonly Button _apply = new() { Text = "应用方案", Width = 100 };
|
|
private readonly Button _delete = new() { Text = "删除方案", Width = 100 };
|
|
private readonly Button _saveConfig = new() { Text = "保存配置", Width = 100 };
|
|
private readonly Button _addPage = new() { Text = "新增换装页", Width = 100 };
|
|
private readonly Button _renamePage = new() { Text = "重命名换装页", Width = 100 };
|
|
private readonly Button _deletePage = new() { Text = "删除换装页", Width = 100 };
|
|
private readonly TabControl _pageTabs = new()
|
|
{
|
|
Dock = DockStyle.Top,
|
|
Height = 34,
|
|
Appearance = TabAppearance.Buttons,
|
|
SizeMode = TabSizeMode.Fixed,
|
|
ItemSize = new Size(120, 28),
|
|
Multiline = false,
|
|
};
|
|
private bool _syncingPageTabs;
|
|
private readonly Config _config;
|
|
private readonly HotkeyManager _hotkeys;
|
|
private readonly Dictionary<string, DateTime> _cooldownUntil = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
public MainForm()
|
|
{
|
|
Text = "DNF 一键换装";
|
|
Width = 720;
|
|
Height = 520;
|
|
StartPosition = FormStartPosition.CenterScreen;
|
|
MinimumSize = new Size(680, 460);
|
|
_config = ConfigStore.Load();
|
|
_server.Text = _config.ServerUrl;
|
|
_token.Text = _config.AdminToken;
|
|
|
|
var top = new TableLayoutPanel { Dock = DockStyle.Top, Height = 46, ColumnCount = 4, Padding = new Padding(8) };
|
|
for (var i = 0; i < 4; i++) top.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
|
top.Controls.Add(new Label { Text = "服务端", AutoSize = true, Anchor = AnchorStyles.Left }, 0, 0);
|
|
top.Controls.Add(_server, 1, 0);
|
|
top.Controls.Add(new Label { Text = "Admin Token", AutoSize = true, Anchor = AnchorStyles.Left }, 2, 0);
|
|
top.Controls.Add(_token, 3, 0);
|
|
|
|
var right = new FlowLayoutPanel { Dock = DockStyle.Right, Width = 130, FlowDirection = FlowDirection.TopDown, Padding = new Padding(6) };
|
|
foreach (var button in new[] { _addPage, _renamePage, _deletePage, _add, _hotkey, _saveCurrent, _apply, _delete, _saveConfig })
|
|
{
|
|
button.Width = 112;
|
|
right.Controls.Add(button);
|
|
}
|
|
|
|
Controls.Add(_status);
|
|
Controls.Add(_schemes);
|
|
Controls.Add(_pageTabs);
|
|
Controls.Add(right);
|
|
Controls.Add(top);
|
|
|
|
_hotkeys = new HotkeyManager(_config);
|
|
_hotkeys.Triggered += scheme => BeginInvoke(async () => await ApplySchemeAsync(scheme));
|
|
|
|
_pageTabs.SelectedIndexChanged += (_, _) => OnPageChanged();
|
|
_addPage.Click += (_, _) => AddPage();
|
|
_renamePage.Click += (_, _) => RenamePage();
|
|
_deletePage.Click += (_, _) => DeletePage();
|
|
_add.Click += (_, _) => AddScheme();
|
|
_hotkey.Click += (_, _) => SetHotkey();
|
|
_saveCurrent.Click += async (_, _) => await SaveCurrentAsync();
|
|
_apply.Click += async (_, _) => await ApplySelectedAsync();
|
|
_delete.Click += (_, _) => DeleteScheme();
|
|
_saveConfig.Click += (_, _) => SaveConfig(true);
|
|
_schemes.DoubleClick += async (_, _) => await ApplySelectedAsync();
|
|
FormClosing += (_, _) => SaveConfig(false);
|
|
RebuildPageTabs();
|
|
RefreshList();
|
|
}
|
|
|
|
private Scheme? Selected => _schemes.SelectedIndex >= 0 && _schemes.SelectedIndex < _config.CurrentPage.Schemes.Count
|
|
? _config.CurrentPage.Schemes[_schemes.SelectedIndex] : null;
|
|
|
|
private void RebuildPageTabs()
|
|
{
|
|
_syncingPageTabs = true;
|
|
_pageTabs.TabPages.Clear();
|
|
foreach (var page in _config.Pages)
|
|
{
|
|
_pageTabs.TabPages.Add(new TabPage(page.Name));
|
|
}
|
|
if (_pageTabs.TabPages.Count > 0)
|
|
_pageTabs.SelectedIndex = Math.Clamp(_config.ActivePage, 0, _pageTabs.TabPages.Count - 1);
|
|
_syncingPageTabs = false;
|
|
}
|
|
|
|
private void OnPageChanged()
|
|
{
|
|
if (_syncingPageTabs || _pageTabs.SelectedIndex < 0) return;
|
|
_config.ActivePage = _pageTabs.SelectedIndex;
|
|
_hotkeys.UpdateConfig(_config);
|
|
RefreshList();
|
|
SaveConfig(false);
|
|
}
|
|
|
|
private void AddPage()
|
|
{
|
|
using var prompt = new TextPrompt("新增换装页", "换装页名称", $"换装页{_config.Pages.Count + 1}");
|
|
if (prompt.ShowDialog(this) != DialogResult.OK || prompt.Value.Length == 0) return;
|
|
_config.Pages.Add(new LoadoutPage { Name = prompt.Value });
|
|
_config.ActivePage = _config.Pages.Count - 1;
|
|
RebuildPageTabs();
|
|
RefreshList();
|
|
SaveConfig(false);
|
|
}
|
|
|
|
private void RenamePage()
|
|
{
|
|
var page = _config.CurrentPage;
|
|
using var prompt = new TextPrompt("重命名换装页", "换装页名称", page.Name);
|
|
if (prompt.ShowDialog(this) != DialogResult.OK || prompt.Value.Length == 0) return;
|
|
page.Name = prompt.Value;
|
|
RebuildPageTabs();
|
|
SaveConfig(false);
|
|
}
|
|
|
|
private void DeletePage()
|
|
{
|
|
if (_config.Pages.Count <= 1)
|
|
{
|
|
_status.Text = "至少保留一个换装页。";
|
|
return;
|
|
}
|
|
var page = _config.CurrentPage;
|
|
if (MessageBox.Show(this, $"删除换装页“{page.Name}”及其中的全部方案?", "确认",
|
|
MessageBoxButtons.YesNo) != DialogResult.Yes) return;
|
|
_config.Pages.Remove(page);
|
|
_config.ActivePage = Math.Clamp(_config.ActivePage, 0, _config.Pages.Count - 1);
|
|
RebuildPageTabs();
|
|
RefreshList();
|
|
SaveConfig(false);
|
|
}
|
|
|
|
private void RefreshList()
|
|
{
|
|
var selected = _schemes.SelectedIndex;
|
|
_schemes.Items.Clear();
|
|
foreach (var scheme in _config.CurrentPage.Schemes) _schemes.Items.Add(scheme.Display);
|
|
if (_schemes.Items.Count > 0) _schemes.SelectedIndex = Math.Clamp(selected, 0, _schemes.Items.Count - 1);
|
|
}
|
|
|
|
private void AddScheme()
|
|
{
|
|
using var namePrompt = new TextPrompt("新增方案", "方案名称", $"方案{_config.CurrentPage.Schemes.Count + 1}");
|
|
if (namePrompt.ShowDialog(this) != DialogResult.OK || namePrompt.Value.Length == 0) return;
|
|
using var characterPrompt = new TextPrompt("绑定角色", "角色游戏内名称", "");
|
|
if (characterPrompt.ShowDialog(this) != DialogResult.OK || characterPrompt.Value.Length == 0) return;
|
|
var scheme = new Scheme
|
|
{
|
|
Name = namePrompt.Value,
|
|
CharacterName = characterPrompt.Value,
|
|
};
|
|
_config.CurrentPage.Schemes.Add(scheme);
|
|
RefreshList();
|
|
_schemes.SelectedIndex = _config.CurrentPage.Schemes.Count - 1;
|
|
SetHotkey();
|
|
}
|
|
|
|
private void SetHotkey()
|
|
{
|
|
var scheme = Selected;
|
|
if (scheme == null) return;
|
|
using var dialog = new HotkeyDialog(scheme.Modifiers, scheme.KeyCode);
|
|
if (dialog.ShowDialog(this) != DialogResult.OK || dialog.KeyCode == 0) return;
|
|
scheme.Modifiers = dialog.Modifiers;
|
|
scheme.KeyCode = dialog.KeyCode;
|
|
RefreshList();
|
|
SaveConfig(false);
|
|
}
|
|
|
|
private async Task SaveCurrentAsync()
|
|
{
|
|
var scheme = Selected;
|
|
if (scheme == null) { MessageBox.Show(this, "先选择一个方案"); return; }
|
|
if (!ValidateConnection()) return;
|
|
if (!EnsureCharacter(scheme)) return;
|
|
try
|
|
{
|
|
SaveConfig(false);
|
|
var client = new ServerClient(_config);
|
|
var slots = await client.GetEquipmentAsync(scheme.CharacterName);
|
|
scheme.Slots = slots;
|
|
RefreshList();
|
|
SaveConfig(false);
|
|
_status.Text = $"已保存方案“{scheme.Name}”:角色“{scheme.CharacterName}”,{slots.Count} 个部位。";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_status.Text = "保存失败:" + ex.Message;
|
|
}
|
|
}
|
|
|
|
private async Task ApplySelectedAsync()
|
|
{
|
|
var scheme = Selected;
|
|
if (scheme == null) { MessageBox.Show(this, "先选择一个方案"); return; }
|
|
await ApplySchemeAsync(scheme);
|
|
}
|
|
|
|
private async Task ApplySchemeAsync(Scheme scheme)
|
|
{
|
|
if (!ValidateConnection()) return;
|
|
if (!EnsureCharacter(scheme)) return;
|
|
if (!CheckCooldown(scheme)) return;
|
|
if (scheme.Slots.Count == 0)
|
|
{
|
|
_status.Text = $"方案“{scheme.Name}”还没有保存配装。";
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
SaveConfig(false);
|
|
_status.Text = $"正在应用“{scheme.Name}”到角色“{scheme.CharacterName}”...";
|
|
var client = new ServerClient(_config);
|
|
var result = await client.ApplyAsync(scheme.CharacterName, scheme.Slots);
|
|
var applied = result.Applied ?? new List<SlotItem>();
|
|
var skippedList = result.Skipped ?? new List<ServerClient.SkipData>();
|
|
var skipped = skippedList.Count == 0 ? "无跳过"
|
|
: "跳过:" + string.Join("; ", skippedList.Select(s => $"槽{s.Slot}={s.ItemId} {s.Reason}"));
|
|
if (result.CooldownSeconds > 0)
|
|
{
|
|
_cooldownUntil[scheme.CharacterName] = DateTime.UtcNow.AddSeconds(result.CooldownSeconds);
|
|
_status.Text = $"应用“{scheme.Name}”完成:成功 {applied.Count} 件,{skipped};换装冷却 {result.CooldownSeconds} 秒"
|
|
+ (result.RefreshError is { Length: > 0 } ? " 刷新异常:" + result.RefreshError : "");
|
|
}
|
|
else
|
|
{
|
|
_status.Text = $"应用“{scheme.Name}”完成:成功 {applied.Count} 件,{skipped}"
|
|
+ (result.RefreshError is { Length: > 0 } ? " 刷新异常:" + result.RefreshError : "");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_status.Text = "应用失败:" + ex.Message;
|
|
}
|
|
}
|
|
|
|
private bool CheckCooldown(Scheme scheme)
|
|
{
|
|
if (_cooldownUntil.TryGetValue(scheme.CharacterName, out var until))
|
|
{
|
|
var remaining = (int)Math.Ceiling((until - DateTime.UtcNow).TotalSeconds);
|
|
if (remaining > 0)
|
|
{
|
|
_status.Text = $"角色“{scheme.CharacterName}”换装冷却中,还需 {remaining} 秒。";
|
|
return false;
|
|
}
|
|
_cooldownUntil.Remove(scheme.CharacterName);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void DeleteScheme()
|
|
{
|
|
var scheme = Selected;
|
|
if (scheme == null) return;
|
|
if (MessageBox.Show(this, $"删除方案“{scheme.Name}”?", "确认", MessageBoxButtons.YesNo) != DialogResult.Yes) return;
|
|
_config.CurrentPage.Schemes.Remove(scheme);
|
|
RefreshList();
|
|
SaveConfig(false);
|
|
}
|
|
|
|
private bool EnsureCharacter(Scheme scheme)
|
|
{
|
|
if (scheme.CharacterName.Trim().Length > 0) return true;
|
|
using var prompt = new TextPrompt("绑定角色", "角色游戏内名称", "");
|
|
if (prompt.ShowDialog(this) != DialogResult.OK || prompt.Value.Length == 0) return false;
|
|
scheme.CharacterName = prompt.Value;
|
|
RefreshList();
|
|
SaveConfig(false);
|
|
return true;
|
|
}
|
|
|
|
private bool ValidateConnection()
|
|
{
|
|
_config.ServerUrl = _server.Text.Trim();
|
|
_config.AdminToken = _token.Text.Trim();
|
|
if (_config.ServerUrl.Length == 0 || _config.AdminToken.Length == 0)
|
|
{
|
|
_status.Text = "请填写服务端地址和 Admin Token。";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void SaveConfig(bool show)
|
|
{
|
|
_config.ServerUrl = _server.Text.Trim();
|
|
_config.AdminToken = _token.Text.Trim();
|
|
if (!_syncingPageTabs && _pageTabs.SelectedIndex >= 0)
|
|
_config.ActivePage = _pageTabs.SelectedIndex;
|
|
ConfigStore.Save(_config);
|
|
_hotkeys.UpdateConfig(_config);
|
|
if (show) _status.Text = "配置已保存。";
|
|
}
|
|
}
|