DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,191 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"pvfine/internal/npk"
|
||||
)
|
||||
|
||||
func writeImageBatchTestNPK(t *testing.T, path string, names ...string) {
|
||||
t.Helper()
|
||||
data := append([]byte("NeoplePack_Bill"), 0)
|
||||
putImageTestInt32(&data, len(names))
|
||||
member := append([]byte("Neople Img File"), 0)
|
||||
putImageTestInt32(&member, 72, 0, 2, 2)
|
||||
for range 2 {
|
||||
putImageTestInt32(&member, 16, 5, 1, 1, 4, 0, 0, 1, 1)
|
||||
}
|
||||
member = append(member, 3, 2, 1, 255, 6, 5, 4, 255)
|
||||
key := []byte("puchikon@neople dungeon and fighter " + strings.Repeat("DNF", 73) + "\x00")
|
||||
for i, name := range names {
|
||||
putImageTestInt32(&data, 20+264*len(names)+i*len(member), len(member))
|
||||
var encrypted [256]byte
|
||||
for j := range encrypted {
|
||||
var value byte
|
||||
if j < len(name) {
|
||||
value = name[j]
|
||||
}
|
||||
encrypted[j] = value ^ key[j%len(key)]
|
||||
}
|
||||
data = append(data, encrypted[:]...)
|
||||
}
|
||||
for range names {
|
||||
data = append(data, member...)
|
||||
}
|
||||
if err := os.WriteFile(path, data, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func imageBatchTestService(t *testing.T, packages int) *ImageService {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
for n := 0; n < packages; n++ {
|
||||
writeImageBatchTestNPK(t, filepath.Join(root, fmt.Sprintf("%d.npk", n)), fmt.Sprintf("sprite/item/%d-a.img", n), fmt.Sprintf("sprite/item/%d-b.img", n))
|
||||
}
|
||||
s := &ImageService{directory: root, generation: 1, cachePath: filepath.Join(t.TempDir(), "index.json")}
|
||||
s.buildIndex(context.Background(), root, 1, time.Now())
|
||||
if s.IndexStatus().State != ImageIndexStateReady {
|
||||
t.Fatal("image index not ready")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func TestImageBatchGroupsPackagesMembersAndDuplicateFrames(t *testing.T) {
|
||||
s := imageBatchTestService(t, 2)
|
||||
var opens, parses atomic.Int32
|
||||
s.batchIO = imageBatchIO{
|
||||
open: func(path string) (*os.File, error) { opens.Add(1); return os.Open(path) },
|
||||
parse: func(r io.ReaderAt, offset, size int64) (*npk.IMGFile, error) {
|
||||
parses.Add(1)
|
||||
return npk.ParseIMGAt(r, offset, size)
|
||||
},
|
||||
}
|
||||
refs := []ImageReference{
|
||||
{Path: "item/0-b.img"}, {Path: "ITEM\\0-A.IMG"}, {Path: "item/1-a.img"},
|
||||
{Path: "sprite/item/0-a.img"}, {Path: "item/0-a.img", Index: 1}, {Path: "item/0-a.img", Index: 9},
|
||||
{Path: "missing.img"}, {Path: "item/0-a.img", Index: -1},
|
||||
}
|
||||
results, err := s.GetImages(refs, 1)
|
||||
if err != nil || len(results) != len(refs) {
|
||||
t.Fatalf("batch result length: %d, %v", len(results), err)
|
||||
}
|
||||
for i, result := range results {
|
||||
if result.Error != "" {
|
||||
t.Fatalf("result %d: %s", i, result.Error)
|
||||
}
|
||||
if i < 5 && (result.Data == nil || result.Data.Width != 1) {
|
||||
t.Fatalf("result %d missing", i)
|
||||
}
|
||||
if i >= 5 && result.Data != nil {
|
||||
t.Fatalf("invalid result %d returned data", i)
|
||||
}
|
||||
}
|
||||
if results[1].Data != results[3].Data {
|
||||
t.Fatal("duplicate frame was decoded separately")
|
||||
}
|
||||
if opens.Load() != 2 || parses.Load() != 3 {
|
||||
t.Fatalf("opens/parses = %d/%d, want 2/3", opens.Load(), parses.Load())
|
||||
}
|
||||
if results[1].Data.DataURL == results[4].Data.DataURL {
|
||||
t.Fatal("different frames returned the same pixels")
|
||||
}
|
||||
_, err = s.GetImages(refs[:5], 1)
|
||||
if err != nil || opens.Load() != 2 || parses.Load() != 3 {
|
||||
t.Fatalf("cache did IO: %v, %d/%d", err, opens.Load(), parses.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageBatchIsolatesErrorsAndRejectsOldGeneration(t *testing.T) {
|
||||
s := imageBatchTestService(t, 2)
|
||||
if err := os.WriteFile(filepath.Join(s.directory, "0.npk"), []byte("corrupt"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
refs := []ImageReference{{Path: "item/0-a.img"}, {Path: "item/1-a.img"}}
|
||||
results, err := s.GetImages(refs, 1)
|
||||
if err != nil || results[0].Error == "" || results[1].Data == nil {
|
||||
t.Fatalf("partial results = %#v, %v", results, err)
|
||||
}
|
||||
results, err = s.GetImages(refs, 0)
|
||||
if err != nil || results[0].Error != "" || results[1].Data != nil {
|
||||
t.Fatalf("stale request = %#v, %v", results, err)
|
||||
}
|
||||
if _, err := s.GetImages(make([]ImageReference, maxImageBatchSize+1), 1); err == nil {
|
||||
t.Fatal("oversize batch accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageBatchDiscardsResultsWhenGenerationChangesDuringRead(t *testing.T) {
|
||||
s := imageBatchTestService(t, 1)
|
||||
s.batchIO.open = func(path string) (*os.File, error) {
|
||||
file, err := os.Open(path)
|
||||
s.mu.Lock()
|
||||
s.generation++
|
||||
s.clearCacheLocked()
|
||||
s.mu.Unlock()
|
||||
return file, err
|
||||
}
|
||||
results, err := s.GetImages([]ImageReference{{Path: "item/0-a.img"}}, 1)
|
||||
if err != nil || results[0].Data != nil || results[0].Error != "" || len(s.cache) != 0 {
|
||||
t.Fatalf("old generation published: %#v, %v, cache=%d", results, err, len(s.cache))
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageBatchBoundsDecodersAcrossConcurrentRequests(t *testing.T) {
|
||||
s := imageBatchTestService(t, 8)
|
||||
var active, peak atomic.Int32
|
||||
entered := make(chan struct{}, 32)
|
||||
release := make(chan struct{})
|
||||
s.batchIO.open = func(path string) (*os.File, error) {
|
||||
value := active.Add(1)
|
||||
for old := peak.Load(); value > old; old = peak.Load() {
|
||||
if peak.CompareAndSwap(old, value) {
|
||||
break
|
||||
}
|
||||
}
|
||||
entered <- struct{}{}
|
||||
<-release
|
||||
active.Add(-1)
|
||||
return os.Open(path)
|
||||
}
|
||||
refs := make([]ImageReference, 8)
|
||||
for i := range refs {
|
||||
refs[i] = ImageReference{Path: fmt.Sprintf("item/%d-a.img", i)}
|
||||
}
|
||||
var requests sync.WaitGroup
|
||||
requests.Add(3)
|
||||
for range 3 {
|
||||
go func() {
|
||||
defer requests.Done()
|
||||
if _, err := s.GetImages(refs, 1); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
defer requests.Wait()
|
||||
defer close(release)
|
||||
for range maxImageDecodeWorkers {
|
||||
select {
|
||||
case <-entered:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("decoders did not start")
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-entered:
|
||||
t.Fatal("decoder concurrency limit exceeded")
|
||||
case <-time.After(20 * time.Millisecond):
|
||||
}
|
||||
if peak.Load() != maxImageDecodeWorkers {
|
||||
t.Fatalf("peak=%d", peak.Load())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user