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

+216
View File
@@ -0,0 +1,216 @@
package services
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"image/png"
"io"
"os"
"path/filepath"
"sort"
"sync"
"pvfine/internal/npk"
)
const maxImageBatchSize = 128
const maxImageDecodeWorkers = 4
type imageBatchIO struct {
open func(string) (*os.File, error)
parse func(io.ReaderAt, int64, int64) (*npk.IMGFile, error)
}
type imageBatchJob struct {
key string
index int32
record *imageRecord
positions []int
}
type imageBatchGroup struct {
path string
jobs []*imageBatchJob
}
// GetImages groups a small visible-image batch by NPK and IMG. It opens each
// package once and parses each requested member's frame table once. Generation
// prevents delayed frontend requests from reading a different resource set.
func (s *ImageService) GetImages(references []ImageReference, generation uint64) ([]ImageBatchResult, error) {
defer debugPhase("GetImages", fmt.Sprintf("frames=%d", len(references)))()
return s.getImages(references, &generation)
}
func (s *ImageService) getImages(references []ImageReference, expectedGeneration *uint64) ([]ImageBatchResult, error) {
if len(references) > maxImageBatchSize {
return nil, errors.New("图像批量请求超过 128 帧")
}
results := make([]ImageBatchResult, len(references))
if len(references) == 0 {
return results, nil
}
s.mu.Lock()
snapshot, generation := s.snapshot, s.generation
if snapshot == nil || s.directory == "" || (expectedGeneration != nil && *expectedGeneration != generation) {
s.mu.Unlock()
return results, nil
}
if s.decodeSlots == nil {
s.decodeSlots = make(chan struct{}, maxImageDecodeWorkers)
}
slots, batchIO := s.decodeSlots, s.batchIO
groups := make([]*imageBatchGroup, 0)
byPackage := make(map[string]*imageBatchGroup)
byKey := make(map[string]*imageBatchJob)
for position, reference := range references {
path := normalizeImagePath(reference.Path)
if path == "" || reference.Index < 0 {
continue
}
record := snapshot.byPath[path]
if record == nil {
continue
}
key := fmt.Sprintf("%d\x00%s\x00%d", generation, record.path, reference.Index)
if cached, ok := s.cache[key]; ok {
data := cached.data
results[position].Data = &data
continue
}
if job := byKey[key]; job != nil {
job.positions = append(job.positions, position)
continue
}
npkPath := filepath.Join(snapshot.directory, filepath.FromSlash(record.npkRelative))
if !isImagePathWithin(snapshot.directory, npkPath) {
results[position].Error = "NPK 索引路径越界"
continue
}
group := byPackage[npkPath]
if group == nil {
group = &imageBatchGroup{path: npkPath}
byPackage[npkPath] = group
groups = append(groups, group)
}
job := &imageBatchJob{key: key, index: reference.Index, record: record, positions: []int{position}}
byKey[key] = job
group.jobs = append(group.jobs, job)
}
s.mu.Unlock()
if batchIO.open == nil {
batchIO.open = os.Open
}
if batchIO.parse == nil {
batchIO.parse = npk.ParseIMGAt
}
queue := make(chan *imageBatchGroup)
var workers sync.WaitGroup
for worker := 0; worker < min(len(groups), maxImageDecodeWorkers); worker++ {
workers.Add(1)
go func() {
defer workers.Done()
for group := range queue {
slots <- struct{}{}
s.readImageBatchGroup(group, snapshot, generation, batchIO, results)
<-slots
}
}()
}
for _, group := range groups {
queue <- group
}
close(queue)
workers.Wait()
if !s.imageSnapshotCurrent(snapshot, generation) {
return make([]ImageBatchResult, len(references)), nil
}
return results, nil
}
func (s *ImageService) imageSnapshotCurrent(snapshot *imageSnapshot, generation uint64) bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.snapshot == snapshot && s.generation == generation
}
func (s *ImageService) readImageBatchGroup(group *imageBatchGroup, snapshot *imageSnapshot, generation uint64, batchIO imageBatchIO, results []ImageBatchResult) {
if !s.imageSnapshotCurrent(snapshot, generation) {
return
}
file, err := batchIO.open(group.path)
if err != nil {
for _, job := range group.jobs {
setImageBatchResult(results, job, ImageBatchResult{Error: fmt.Sprintf("读取 NPK 文件失败: %v", err)})
}
return
}
defer file.Close()
// Adjacent jobs reuse a frame table; only one IMG table lives per worker.
sort.SliceStable(group.jobs, func(i, j int) bool {
if group.jobs[i].record.offset != group.jobs[j].record.offset {
return group.jobs[i].record.offset < group.jobs[j].record.offset
}
return group.jobs[i].record.path < group.jobs[j].record.path
})
var current *imageRecord
var img *npk.IMGFile
var parseErr error
for _, job := range group.jobs {
if !s.imageSnapshotCurrent(snapshot, generation) {
return
}
if current != job.record {
current = job.record
img = nil
img, parseErr = batchIO.parse(file, current.offset, current.size)
}
if parseErr != nil {
setImageBatchResult(results, job, ImageBatchResult{Error: fmt.Sprintf("读取 IMG 帧表失败: %v", parseErr)})
continue
}
if job.index >= int32(len(img.Images)) {
continue
}
data, size, err := decodeBatchImage(file, current, img, job.index)
if err != nil {
setImageBatchResult(results, job, ImageBatchResult{Error: err.Error()})
continue
}
s.mu.Lock()
if s.snapshot == snapshot && s.generation == generation {
s.putCacheLocked(job.key, *data, size)
setImageBatchResult(results, job, ImageBatchResult{Data: data})
}
s.mu.Unlock()
}
}
func setImageBatchResult(results []ImageBatchResult, job *imageBatchJob, result ImageBatchResult) {
for _, position := range job.positions {
results[position] = result
}
}
func decodeBatchImage(reader io.ReaderAt, record *imageRecord, img *npk.IMGFile, index int32) (*ImageData, int, error) {
finishDecode := debugPhase("GetImages.read-and-decode", fmt.Sprintf("frame=%d", index))
decoded, err := npk.DecodeImage(reader, record.offset, record.size, img, index)
finishDecode()
if err != nil {
return nil, 0, fmt.Errorf("解码 IMG 图片失败: %w", err)
}
var encoded bytes.Buffer
finishEncode := debugPhase("GetImages.png-encode", fmt.Sprintf("frame=%d", index))
if err := png.Encode(&encoded, decoded); err != nil {
finishEncode()
return nil, 0, fmt.Errorf("编码 PNG 失败: %w", err)
}
finishEncode()
bounds := decoded.Bounds()
data := &ImageData{
DataURL: "data:image/png;base64," + base64.StdEncoding.EncodeToString(encoded.Bytes()),
Width: int32(bounds.Dx()), Height: int32(bounds.Dy()),
}
return data, encoded.Len(), nil
}