DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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

No files matched your search

@@ -0,0 +1,33 @@
import { describe,expect,it,vi } from 'vitest';
import { createItemSkillCatalog } from '../src/gui/itemSkillCatalog';
import type { SkillTreeSkill } from '../bindings/pvfine/services/models';
const entry=(id:number,name:string):SkillTreeSkill=>({id,name,path:`skill/test/${id}.skl`,fileIndex:id});
function deferred<T>(){let resolve!:(value:T)=>void;const promise=new Promise<T>(r=>resolve=r);return{resolve,promise};}
describe('物品技能选择归档会话',()=>{
it('技能 ID 按职业隔离,合并重复职业查询,同一归档复用缓存',async()=>{
const read=vi.fn(async(job:string)=>[entry(12,job==='[swordman]'?'鬼斩':'魔法弹')]),session=createItemSkillCatalog(read);
await session.load(['[swordman]','[mage]','[swordman]','[all]','invalid']);
expect(read).toHaveBeenCalledTimes(2);expect(session.skills.value['[swordman]']![0]!.name).toBe('鬼斩');expect(session.skills.value['[mage]']![0]!.name).toBe('魔法弹');
await session.load(['[swordman]']);expect(read).toHaveBeenCalledTimes(2);
});
it('切换职业时延迟返回不会覆盖新请求,旧职业仍可重新读取',async()=>{
const old=deferred<SkillTreeSkill[]>(),next=deferred<SkillTreeSkill[]>();const session=createItemSkillCatalog(job=>job==='[swordman]'?old.promise:next.promise);
const first=session.load(['[swordman]']),second=session.load(['[mage]']);next.resolve([entry(12,'新技能')]);await second;old.resolve([entry(12,'旧技能')]);await first;
expect(session.skills.value).toEqual({'[mage]':[entry(12,'新技能')]});
await session.load(['[swordman]']);expect(session.skills.value['[swordman]']![0]!.name).toBe('旧技能');
});
it('切换归档清空缓存并忽略旧归档响应',async()=>{
const pending=deferred<SkillTreeSkill[]>(),session=createItemSkillCatalog(()=>pending.promise);const request=session.load(['[swordman]']);
session.invalidate(true);pending.resolve([entry(1,'旧归档')]);await request;
expect(session.skills.value).toEqual({});expect(session.loading.value).toBe(false);
});
it('部分职业失败不丢失成功结果,可以重试失败职业',async()=>{
let retry=false;const read=vi.fn(async(job:string)=>{if(job==='[mage]'&&!retry)throw new Error('failed');return[entry(1,job)];});
const session=createItemSkillCatalog(read);await session.load(['[mage]','[swordman]']);expect(session.errors.value['[mage]']).toContain('failed');expect(session.skills.value['[swordman]']).toHaveLength(1);
retry=true;await session.load(['[mage]','[swordman]']);expect(read).toHaveBeenCalledTimes(3);expect(session.errors.value).toEqual({});
});
it('空列表和非职业标签不触发查询,空职业列表也会缓存',async()=>{
const read=vi.fn(async()=>null),session=createItemSkillCatalog(read);await session.load(['[all]','','[bad]\n[tag]']);expect(read).not.toHaveBeenCalled();
await session.load(['[mage]']);await session.load(['[mage]']);expect(read).toHaveBeenCalledTimes(1);expect(session.skills.value['[mage]']).toEqual([]);
});
});