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,198 @@
|
||||
package companion
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
// Profile is developer-owned evidence, never a client-submitted capability.
|
||||
type Profile struct {
|
||||
ID string
|
||||
Version uint32
|
||||
EvidenceID string
|
||||
ClientSHA256 string
|
||||
PVFSHA256 string
|
||||
Job uint8
|
||||
GrowType uint8
|
||||
BasicAttackVerified bool
|
||||
SkillLevelLimits map[int64]int
|
||||
}
|
||||
|
||||
// There are no accepted native profiles yet. Tests use synthetic profiles
|
||||
// only to exercise policies; those profiles are not shipped in this registry.
|
||||
func VerifiedProfiles() []Profile { return nil }
|
||||
|
||||
func validHash(value string) bool {
|
||||
decoded, err := hex.DecodeString(value)
|
||||
return err == nil && len(decoded) == 32 && hex.EncodeToString(decoded) == value
|
||||
}
|
||||
|
||||
func (p Profile) validate(client, pvf string) error {
|
||||
if !validIdentity(p.ID) || p.Version == 0 || !validIdentity(p.EvidenceID) || !p.BasicAttackVerified {
|
||||
return ErrProfile
|
||||
}
|
||||
if !validHash(client) || !validHash(pvf) || client != p.ClientSHA256 || pvf != p.PVFSHA256 {
|
||||
return ErrAssets
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Selection struct {
|
||||
AccountID string
|
||||
OwnerCharacterID string
|
||||
SourceCharacterID string
|
||||
OnlineStateKnown bool
|
||||
SourceOnline bool
|
||||
ClientSHA256 string
|
||||
PVFSHA256 string
|
||||
}
|
||||
|
||||
type EquipmentSpec struct {
|
||||
Slot int16 `json:"slot"`
|
||||
ItemID int64 `json:"item_id"`
|
||||
}
|
||||
type SkillSpec struct {
|
||||
ID int64 `json:"id"`
|
||||
Level int `json:"level"`
|
||||
}
|
||||
type Exclusion struct {
|
||||
Kind string `json:"kind"`
|
||||
ID int64 `json:"id,omitempty"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// Snapshot contains declarative template IDs, not inventory serials, raw
|
||||
// item-instance bytes, live pointers, source cooldowns, wallets or quests.
|
||||
// It is NOT the native ABI or a claim of gear/stat projection parity.
|
||||
type Snapshot struct {
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
AccountID string `json:"account_id"`
|
||||
OwnerCharacterID string `json:"owner_character_id"`
|
||||
SourceCharacterID string `json:"source_character_id"`
|
||||
Name string `json:"name"`
|
||||
Job uint8 `json:"job"`
|
||||
GrowType uint8 `json:"grow_type"`
|
||||
Level int `json:"level"`
|
||||
ProfileID string `json:"profile_id"`
|
||||
ProfileVersion uint32 `json:"profile_version"`
|
||||
ClientSHA256 string `json:"client_sha256"`
|
||||
PVFSHA256 string `json:"pvf_sha256"`
|
||||
Equipment []EquipmentSpec `json:"equipment"`
|
||||
Skills []SkillSpec `json:"skills"`
|
||||
Exclusions []Exclusion `json:"exclusions"`
|
||||
}
|
||||
|
||||
// Prepare consumes a single transaction's detached source state. The caller
|
||||
// must keep the source offline check valid through preparation/run binding.
|
||||
// It does not load or save repositories and cannot enable a native adapter.
|
||||
func Prepare(source repository.CompanionSourceState, s Selection, p Profile, now time.Time) (Snapshot, error) {
|
||||
if !validIdentity(s.AccountID) || !validCharacterID(s.OwnerCharacterID) || !validCharacterID(s.SourceCharacterID) {
|
||||
return Snapshot{}, ErrSelection
|
||||
}
|
||||
if s.OwnerCharacterID == s.SourceCharacterID {
|
||||
return Snapshot{}, ErrSelf
|
||||
}
|
||||
if source.Owner.CharacterID != s.OwnerCharacterID || source.Source.CharacterID != s.SourceCharacterID ||
|
||||
source.Owner.Stats["delete_flag"] != 0 || source.Source.Stats["delete_flag"] != 0 {
|
||||
return Snapshot{}, ErrSource
|
||||
}
|
||||
if source.Owner.AccountID != s.AccountID || source.Source.AccountID != s.AccountID {
|
||||
return Snapshot{}, ErrAccount
|
||||
}
|
||||
if !s.OnlineStateKnown {
|
||||
return Snapshot{}, ErrOnlineUnknown
|
||||
}
|
||||
if s.SourceOnline {
|
||||
return Snapshot{}, ErrOnline
|
||||
}
|
||||
if err := p.validate(s.ClientSHA256, s.PVFSHA256); err != nil {
|
||||
return Snapshot{}, err
|
||||
}
|
||||
job, err := strconv.ParseUint(source.Source.Job, 10, 8)
|
||||
grow, hasGrow := source.Source.Stats["grow_type"]
|
||||
if err != nil || !hasGrow || grow < 0 || grow > 255 || uint8(job) != p.Job || uint8(grow) != p.GrowType {
|
||||
return Snapshot{}, ErrProfile
|
||||
}
|
||||
if now.IsZero() || source.Source.Level < 1 || source.Source.Level > 90 ||
|
||||
!validIdentity(source.Source.Name) || !utf8.ValidString(source.Source.Name) || !source.HasEquipment || !source.HasSkill ||
|
||||
source.Equipment.CharacterID != s.SourceCharacterID || source.Skill.CharacterID != s.SourceCharacterID {
|
||||
return Snapshot{}, ErrSnapshot
|
||||
}
|
||||
if len(source.Equipment.Entries) > 64 || len(source.Skill.Skills) > 1024 {
|
||||
return Snapshot{}, ErrSnapshot
|
||||
}
|
||||
out := Snapshot{SchemaVersion: 1, AccountID: s.AccountID, OwnerCharacterID: s.OwnerCharacterID,
|
||||
SourceCharacterID: s.SourceCharacterID, Name: source.Source.Name, Job: uint8(job), GrowType: uint8(grow),
|
||||
Level: source.Source.Level, ProfileID: p.ID, ProfileVersion: p.Version,
|
||||
ClientSHA256: s.ClientSHA256, PVFSHA256: s.PVFSHA256,
|
||||
Equipment: []EquipmentSpec{}, Skills: []SkillSpec{}, Exclusions: []Exclusion{
|
||||
{Kind: "attributes", Reason: "native_base_stats_and_equipment_projection_not_verified"},
|
||||
{Kind: "equipment", Reason: "instance_options_set_and_proc_effects_not_projected"},
|
||||
}}
|
||||
slots := map[int16]bool{}
|
||||
for _, item := range source.Equipment.Entries {
|
||||
if item.SlotIndex < 0 || item.SlotIndex > 255 || item.ItemID <= 0 || item.ItemID > int64(^uint32(0)) || slots[item.SlotIndex] {
|
||||
return Snapshot{}, ErrSnapshot
|
||||
}
|
||||
slots[item.SlotIndex] = true
|
||||
if !item.ExpireAt.IsZero() && !item.ExpireAt.After(now) {
|
||||
out.Exclusions = append(out.Exclusions, Exclusion{Kind: "equipment", ID: item.ItemID, Reason: "expired"})
|
||||
continue
|
||||
}
|
||||
out.Equipment = append(out.Equipment, EquipmentSpec{Slot: item.SlotIndex, ItemID: item.ItemID})
|
||||
}
|
||||
for id, skill := range source.Skill.Skills {
|
||||
if id <= 0 || id > 65535 || skill.Level < 0 || skill.Level > 65535 {
|
||||
return Snapshot{}, ErrSnapshot
|
||||
}
|
||||
if !skill.Enabled || skill.Level == 0 {
|
||||
continue
|
||||
}
|
||||
limit, supported := p.SkillLevelLimits[id]
|
||||
if !supported || limit <= 0 || skill.Level > limit {
|
||||
out.Exclusions = append(out.Exclusions, Exclusion{Kind: "skill", ID: id, Reason: "native_skill_or_level_unverified"})
|
||||
continue
|
||||
}
|
||||
out.Skills = append(out.Skills, SkillSpec{ID: id, Level: skill.Level})
|
||||
}
|
||||
sort.Slice(out.Equipment, func(i, j int) bool { return out.Equipment[i].Slot < out.Equipment[j].Slot })
|
||||
sort.Slice(out.Skills, func(i, j int) bool { return out.Skills[i].ID < out.Skills[j].ID })
|
||||
sort.Slice(out.Exclusions, func(i, j int) bool {
|
||||
a, b := out.Exclusions[i], out.Exclusions[j]
|
||||
if a.Kind != b.Kind {
|
||||
return a.Kind < b.Kind
|
||||
}
|
||||
if a.ID != b.ID {
|
||||
return a.ID < b.ID
|
||||
}
|
||||
return a.Reason < b.Reason
|
||||
})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s Snapshot) Clone() Snapshot {
|
||||
if s.Equipment != nil {
|
||||
s.Equipment = append([]EquipmentSpec{}, s.Equipment...)
|
||||
}
|
||||
if s.Skills != nil {
|
||||
s.Skills = append([]SkillSpec{}, s.Skills...)
|
||||
}
|
||||
if s.Exclusions != nil {
|
||||
s.Exclusions = append([]Exclusion{}, s.Exclusions...)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Fingerprint is content addressing, NOT an authentication token.
|
||||
func (s Snapshot) Fingerprint() string {
|
||||
body, _ := json.Marshal(s)
|
||||
sum := sha256.Sum256(body)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
Reference in new issue
Block a user