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,272 @@
package guild
import (
"context"
"encoding/json"
"errors"
"math"
"time"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
const (
ContributionCoinID uint32 = 490003262
ContributionDungeon = "guild dungeon clear"
ContributionChat = "guild chat"
ContributionAttendance = "guild attendance"
ContributionParty = "party dungeon clear"
)
var ErrContributionInventoryFull = errors.New("guild contribution coin inventory is full")
type ContributionRule struct{ Required, Points uint32 }
type ContributionCatalog map[string]ContributionRule
// Point amounts and activity parameters come exclusively from the deployed PVF.
func LoadContributionCatalog(source dnfpvf.Source) (ContributionCatalog, error) {
doc, err := readWarDocument(source, "etc/chn_hongbao/chn_hongbao.etc")
if err != nil {
return nil, err
}
t, ok := doc.Section("hongbao point reward")
if !ok || len(t) != 12 {
return nil, ErrInvalidContent
}
c := make(ContributionCatalog)
for i := 0; i < len(t); i += 3 {
kind := t[i].Value
switch kind {
case ContributionDungeon, ContributionChat, ContributionAttendance, ContributionParty:
default:
return nil, ErrInvalidContent
}
v, e := warUnsigned(t[i+1 : i+3])
if e != nil || len(v) != 2 || v[0] == 0 || v[1] == 0 || v[0] > 10000 || v[1] > 10000 || c[kind].Required != 0 {
return nil, ErrInvalidContent
}
c[kind] = ContributionRule{v[0], v[1]}
}
return c, nil
}
// These economy choices are local server policy, not fields inferred from PVF.
type ContributionPolicy struct {
CoinsPerPoint uint32 `json:"coins_per_point"`
RewardsPerActivityPerDay uint32 `json:"rewards_per_activity_per_day"`
ResetHour int `json:"reset_hour"`
}
const ContributionPolicyScope = "global:guild_contribution_policy:v1"
func LoadContributionPolicy(ctx context.Context, repo dnfrepo.SettingsRepository) (*ContributionPolicy, error) {
if repo == nil {
return nil, ErrRepositoryUnavailable
}
r, found, err := repo.Load(ctx, ContributionPolicyScope)
if err != nil || !found {
return nil, err
}
var p ContributionPolicy
if err = json.Unmarshal([]byte(r.Values["state"]), &p); err != nil {
return nil, err
}
if !p.valid() {
return nil, ErrInvalidContent
}
return &p, nil
}
func SaveContributionPolicy(ctx context.Context, repo dnfrepo.SettingsRepository, p ContributionPolicy, now time.Time) error {
if repo == nil || !p.valid() {
return ErrInvalidContent
}
b, err := json.Marshal(p)
if err != nil {
return err
}
return repo.Save(ctx, dnfrepo.SettingsRecord{Scope: ContributionPolicyScope, Values: map[string]string{"state": string(b)}, UpdatedAt: now})
}
func (p ContributionPolicy) valid() bool {
return p.CoinsPerPoint > 0 && p.CoinsPerPoint <= 10000 && p.RewardsPerActivityPerDay > 0 && p.RewardsPerActivityPerDay <= 100 && p.ResetHour >= 0 && p.ResetHour < 24
}
func (p ContributionPolicy) Day(now time.Time) time.Time {
n := now.In(time.FixedZone("CST", 8*3600))
d := time.Date(n.Year(), n.Month(), n.Day(), p.ResetHour, 0, 0, 0, n.Location())
if n.Before(d) {
d = d.AddDate(0, 0, -1)
}
return d
}
// The character ledger survives leaving a guild: rejoining does not reset daily
// limits or erase earned, undelivered coins. It never imports guild Mileage.
type ContributionState struct {
Version int `json:"version"`
DayStart int64 `json:"day_start"`
Progress map[string]uint32 `json:"progress,omitempty"`
Rewarded map[string]uint32 `json:"rewarded,omitempty"`
Receipts map[string]bool `json:"receipts,omitempty"`
Points uint64 `json:"points"`
PendingCoins uint32 `json:"pending_coins"`
PaidCoins uint64 `json:"paid_coins"`
}
func contributionScope(id string) string { return "character:" + id + ":guild_contribution:v1" }
func LoadContribution(ctx context.Context, repo dnfrepo.SettingsRepository, id string) (ContributionState, error) {
s := ContributionState{Version: 1}
if repo == nil || id == "" {
return s, ErrRepositoryUnavailable
}
r, found, err := repo.Load(ctx, contributionScope(id))
if err != nil {
return s, err
}
if found && r.Values["state"] != "" {
if err = json.Unmarshal([]byte(r.Values["state"]), &s); err != nil {
return s, err
}
if s.Version != 1 {
return s, ErrInvalidContent
}
}
return s, nil
}
func SaveContribution(ctx context.Context, repo dnfrepo.SettingsRepository, id string, s ContributionState, now time.Time) error {
if repo == nil || id == "" || s.Version != 1 {
return ErrRepositoryUnavailable
}
b, err := json.Marshal(s)
if err != nil {
return err
}
return repo.Save(ctx, dnfrepo.SettingsRecord{Scope: contributionScope(id), Values: map[string]string{"state": string(b)}, UpdatedAt: now})
}
// Observe accepts server-owned events. The party parameter is the distinct
// same-guild participant count; other activities count successful occurrences.
func (s *ContributionState) Observe(c ContributionCatalog, p ContributionPolicy, kind, receipt string, partyMembers uint32, occurredAt, now time.Time) (uint32, error) {
rule, ok := c[kind]
if s == nil || !p.valid() || !ok || rule.Required == 0 || rule.Points == 0 || receipt == "" || occurredAt.After(now) {
return 0, ErrInvalidContent
}
day := p.Day(now).Unix()
if occurredAt.Unix() < day || s.DayStart > day {
return 0, nil
}
if s.DayStart != day {
s.DayStart = day
s.Progress = nil
s.Rewarded = nil
s.Receipts = nil
}
if s.Progress == nil {
s.Progress = make(map[string]uint32)
}
if s.Rewarded == nil {
s.Rewarded = make(map[string]uint32)
}
if s.Receipts == nil {
s.Receipts = make(map[string]bool)
}
key := kind + ":" + receipt
if s.Receipts[key] || s.Rewarded[kind] >= p.RewardsPerActivityPerDay {
return 0, nil
}
if kind == ContributionParty && partyMembers < rule.Required {
return 0, nil
}
if len(s.Receipts) >= 10000 {
return 0, ErrInvalidContent
}
s.Receipts[key] = true
s.Progress[kind]++
required := rule.Required
if kind == ContributionParty {
required = 1
}
if s.Progress[kind] < required {
return 0, nil
}
coins := uint64(rule.Points) * uint64(p.CoinsPerPoint)
if coins > math.MaxUint32-uint64(s.PendingCoins) || s.Points > math.MaxUint64-uint64(rule.Points) {
return 0, ErrInvalidContent
}
s.Progress[kind] = 0
s.Rewarded[kind]++
s.Points += uint64(rule.Points)
s.PendingCoins += uint32(coins)
s.Version = 1
return rule.Points, nil
}
// Deliver is called within the same repository transaction as the ledger save.
// A grant may partially modify its input before reporting full; clone first.
func (s *ContributionState) Deliver(inventory *dnfrepo.InventoryRecord, grant func(*dnfrepo.InventoryRecord, uint32) error) (bool, error) {
if s.PendingCoins == 0 {
return false, nil
}
if inventory == nil || grant == nil || s.PaidCoins > math.MaxUint64-uint64(s.PendingCoins) {
return false, ErrInvalidContent
}
next := dnfrepo.CloneInventory(*inventory)
if err := grant(&next, s.PendingCoins); err != nil {
if errors.Is(err, ErrContributionInventoryFull) {
return false, nil
}
return false, err
}
*inventory = next
s.PaidCoins += uint64(s.PendingCoins)
s.PendingCoins = 0
return true, nil
}
type ContributionEntry struct {
GuildID uint32
CharacterID string
Members map[string]time.Time
}
func (r Registry) ContributionEntry(id string) ContributionEntry {
g, _, ok := r.GuildForCharacter(id)
if !ok {
return ContributionEntry{}
}
e := ContributionEntry{GuildID: g.ID, CharacterID: id, Members: make(map[string]time.Time, len(g.Members))}
for id, m := range g.Members {
e.Members[id] = m.JoinedAt
}
return e
}
func (r Registry) ContributionEligible(e ContributionEntry, party []string) (bool, uint32) {
g, m, ok := r.GuildForCharacter(e.CharacterID)
if !ok || g.ID != e.GuildID || !m.JoinedAt.Equal(e.Members[e.CharacterID]) {
return false, 0
}
seen := make(map[string]bool)
for _, id := range party {
member, ok := g.Members[id]
joined, entered := e.Members[id]
if ok && entered && member.JoinedAt.Equal(joined) {
seen[id] = true
}
}
if !seen[e.CharacterID] {
return true, 0
}
return true, uint32(len(seen))
}
func (r *Registry) AddContributionPoints(id string, points uint32, now time.Time) error {
g, m, ok := r.GuildForCharacter(id)
if !ok {
return ErrNotMember
}
if points > math.MaxUint32-m.ContributionPoints {
return ErrInvalidContent
}
m.ContributionPoints += points
g.Members[id] = m
g.UpdatedAt = now
return nil
}