服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
204 lines
6.6 KiB
Go
204 lines
6.6 KiB
Go
package pvf
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"os"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func workspaceReferenceInput() StringReferenceInput {
|
|
tokens := []struct {
|
|
typ byte
|
|
offset int32
|
|
}{{3, 20}, {5, 30}, {7, 30}, {5, 30}, {10, -7}, {6, 20}, {8, 20}, {0, 111}}
|
|
raw := make([]byte, len(tokens)*5)
|
|
for i, token := range tokens {
|
|
raw[i*5] = token.typ
|
|
binary.LittleEndian.PutUint32(raw[i*5+1:], uint32(token.offset))
|
|
}
|
|
// An incomplete trailing token is ignored by the existing parser.
|
|
raw = append(raw, 7, 1)
|
|
return StringReferenceInput{raw: raw, nameOff: 20, pathOff: 20}
|
|
}
|
|
|
|
func TestStringReferenceWorkspaceReusesWithoutStaleReferences(t *testing.T) {
|
|
input := workspaceReferenceInput()
|
|
want := []StringReference{
|
|
{Offset: -7, Occurrences: 1, TokenTypes: []int32{10}},
|
|
{Offset: 20, Occurrences: 5, TokenTypes: []int32{3, 6, 8}, FileFields: []string{"name", "path"}},
|
|
{Offset: 30, Occurrences: 3, TokenTypes: []int32{5, 7}},
|
|
}
|
|
var workspace StringReferenceWorkspace
|
|
refs, err := workspace.References(context.Background(), input)
|
|
if err != nil || !reflect.DeepEqual(refs, want) {
|
|
t.Fatalf("unexpected references: %v, err=%v", refs, err)
|
|
}
|
|
storage := &refs[0]
|
|
workspace.Reset()
|
|
if len(workspace.refs) != 0 || len(workspace.byOffset) != 0 || cap(workspace.refs) == 0 {
|
|
t.Fatal("reset did not clear logical state and retain small storage")
|
|
}
|
|
refs, err = workspace.References(context.Background(), StringReferenceInput{nameOff: 30, pathOff: -7})
|
|
want = []StringReference{
|
|
{Offset: -7, Occurrences: 1, TokenTypes: []int32{}, FileFields: []string{"path"}},
|
|
{Offset: 30, Occurrences: 1, TokenTypes: []int32{}, FileFields: []string{"name"}},
|
|
}
|
|
// Empty slices may reuse prior capacity; only their contents are semantic.
|
|
if err != nil || len(refs) != len(want) {
|
|
t.Fatalf("second parse failed: refs=%d err=%v", len(refs), err)
|
|
}
|
|
for i, ref := range refs {
|
|
if ref.Offset != want[i].Offset || ref.Occurrences != want[i].Occurrences || len(ref.TokenTypes) != 0 || !reflect.DeepEqual(ref.FileFields, want[i].FileFields) {
|
|
t.Fatal("second parse retained stale references, token types or fields")
|
|
}
|
|
}
|
|
if storage != &refs[0] {
|
|
t.Fatal("small result storage was not reused")
|
|
}
|
|
// The original owned-result API must remain independent of workspace reuse
|
|
// and later calls, including nested slices.
|
|
owned, err := input.References(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other, err := input.References(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other[0].TokenTypes[0] = 99
|
|
other[1].FileFields[0] = "changed"
|
|
if owned[0].TokenTypes[0] != 10 || owned[1].FileFields[0] != "name" {
|
|
t.Fatal("owned results share nested storage")
|
|
}
|
|
}
|
|
|
|
func largeWorkspaceReferenceInput() StringReferenceInput {
|
|
raw := make([]byte, (maxRetainedStringReferences+10)*5)
|
|
for i := 0; i < len(raw)/5; i++ {
|
|
raw[i*5] = 7
|
|
binary.LittleEndian.PutUint32(raw[i*5+1:], uint32(i))
|
|
}
|
|
return StringReferenceInput{raw: raw, nameOff: -1, pathOff: -2}
|
|
}
|
|
|
|
func TestStringReferenceWorkspaceDiscardsOversizedStorage(t *testing.T) {
|
|
var workspace StringReferenceWorkspace
|
|
refs, err := workspace.References(context.Background(), largeWorkspaceReferenceInput())
|
|
if err != nil || len(refs) != maxRetainedStringReferences+12 {
|
|
t.Fatalf("large file was truncated: refs=%d err=%v", len(refs), err)
|
|
}
|
|
workspace.Reset()
|
|
if workspace.refs != nil || workspace.byOffset != nil {
|
|
t.Fatal("oversized workspace survived reset")
|
|
}
|
|
refs, err = workspace.References(context.Background(), StringReferenceInput{nameOff: 1, pathOff: 2})
|
|
if err != nil || len(refs) != 2 || cap(workspace.refs) > maxRetainedStringReferences {
|
|
t.Fatal("small parse did not recover after oversized result")
|
|
}
|
|
}
|
|
|
|
type workspaceCancelContext struct {
|
|
context.Context
|
|
checks int
|
|
}
|
|
|
|
func (ctx *workspaceCancelContext) Err() error {
|
|
ctx.checks++
|
|
if ctx.checks >= 6 {
|
|
return context.Canceled
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestStringReferenceWorkspaceCancellationReleasesPartialResult(t *testing.T) {
|
|
var workspace StringReferenceWorkspace
|
|
ctx := &workspaceCancelContext{Context: context.Background()}
|
|
refs, err := workspace.References(ctx, largeWorkspaceReferenceInput())
|
|
if !errors.Is(err, context.Canceled) || refs != nil {
|
|
t.Fatalf("partial parse was published: refs=%d err=%v", len(refs), err)
|
|
}
|
|
if workspace.refs != nil || workspace.byOffset != nil {
|
|
t.Fatal("canceled parse retained oversized storage")
|
|
}
|
|
refs, err = workspace.References(context.Background(), workspaceReferenceInput())
|
|
if err != nil || len(refs) != 3 {
|
|
t.Fatal("workspace could not recover after cancellation")
|
|
}
|
|
canceled, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
refs, err = workspace.References(canceled, workspaceReferenceInput())
|
|
if !errors.Is(err, context.Canceled) || refs != nil || len(workspace.refs) != 0 || len(workspace.byOffset) != 0 {
|
|
t.Fatal("already canceled parse retained previous logical state")
|
|
}
|
|
}
|
|
|
|
// Opt-in full-file comparison against the independently owned parser. It logs
|
|
// only counts and never archive paths, string-pool values or payload bytes.
|
|
func TestStringReferenceWorkspaceRealArchive(t *testing.T) {
|
|
if os.Getenv("PVF_REFERENCE_TEST") != "1" {
|
|
t.Skip("PVF_REFERENCE_TEST=1 required")
|
|
}
|
|
path := os.Getenv("PVF_TESTFILE")
|
|
if path == "" {
|
|
t.Fatal("PVF_TESTFILE required")
|
|
}
|
|
a, err := Open(path)
|
|
if err != nil {
|
|
t.Fatal("cannot open reference test archive")
|
|
}
|
|
ctx := context.Background()
|
|
scanner := a.NewStringReferenceScanner()
|
|
var workspace StringReferenceWorkspace
|
|
files, count := 0, 0
|
|
for {
|
|
index, input, ok, err := scanner.NextInput(ctx)
|
|
if err != nil {
|
|
t.Fatal("cannot read reference test input")
|
|
}
|
|
if !ok {
|
|
break
|
|
}
|
|
want, err := input.References(ctx)
|
|
if err != nil {
|
|
t.Fatalf("owned parser failed for file %d", index)
|
|
}
|
|
refs, err := workspace.References(ctx, input)
|
|
if err != nil || !equalWorkspaceReferences(refs, want) {
|
|
t.Fatalf("reference mismatch for file %d", index)
|
|
}
|
|
files++
|
|
count += len(refs)
|
|
workspace.Reset()
|
|
}
|
|
if files != int(a.FileCount()) {
|
|
t.Fatalf("missing files: got=%d want=%d", files, a.FileCount())
|
|
}
|
|
t.Logf("files=%d references=%d", files, count)
|
|
}
|
|
|
|
func equalWorkspaceReferences(left, right []StringReference) bool {
|
|
if len(left) != len(right) {
|
|
return false
|
|
}
|
|
for i, ref := range left {
|
|
other := right[i]
|
|
if ref.Offset != other.Offset || ref.Occurrences != other.Occurrences || len(ref.TokenTypes) != len(other.TokenTypes) || len(ref.FileFields) != len(other.FileFields) {
|
|
return false
|
|
}
|
|
for j, typ := range ref.TokenTypes {
|
|
if typ != other.TokenTypes[j] {
|
|
return false
|
|
}
|
|
}
|
|
for j, field := range ref.FileFields {
|
|
if field != other.FileFields[j] {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|