Files
90CNServer/loadout-tool/MainForm.cs
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

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 = "配置已保存。";
}
}