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,219 @@
|
||||
package guild
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
var ErrWarMissionIncomplete = errors.New("guild war mission is incomplete")
|
||||
|
||||
type WarMissionReward struct{ ItemID, Count uint32 }
|
||||
type WarMission struct {
|
||||
ID uint32
|
||||
Season bool
|
||||
Title, Condition string
|
||||
Required uint32
|
||||
Dungeons []uint32
|
||||
Rewards []WarMissionReward
|
||||
}
|
||||
type WarMissionCatalog struct {
|
||||
Missions []WarMission
|
||||
DailyTitle, SeasonTitle uint32
|
||||
}
|
||||
|
||||
func (c *WarMissionCatalog) Find(id uint32) (WarMission, bool) {
|
||||
if c != nil {
|
||||
for _, m := range c.Missions {
|
||||
if m.ID == id {
|
||||
return m, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return WarMission{}, false
|
||||
}
|
||||
|
||||
type WarMissionValue struct {
|
||||
Progress uint32 `json:"progress"`
|
||||
Claimed bool `json:"claimed,omitempty"`
|
||||
}
|
||||
type WarMissionState struct {
|
||||
DayStart int64 `json:"day_start"`
|
||||
SeasonStart int64 `json:"season_start"`
|
||||
Values map[uint32]WarMissionValue `json:"values"`
|
||||
Events map[string]int64 `json:"events,omitempty"`
|
||||
}
|
||||
|
||||
// Guild war days reset at 06:00 Beijing. No historical progress is synthesized.
|
||||
func WarDay(now time.Time) (start, end time.Time) {
|
||||
local := now.In(time.FixedZone("CST", 8*60*60))
|
||||
start = time.Date(local.Year(), local.Month(), local.Day(), 6, 0, 0, 0, local.Location())
|
||||
if local.Before(start) {
|
||||
start = start.AddDate(0, 0, -1)
|
||||
}
|
||||
return start, start.AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
func (s *WarMissionState) Reset(now time.Time, catalog *WarMissionCatalog) {
|
||||
day, _ := WarDay(now)
|
||||
season, _ := WarSeason(now)
|
||||
if s.Values == nil {
|
||||
s.Values = make(map[uint32]WarMissionValue)
|
||||
}
|
||||
if s.Events == nil {
|
||||
s.Events = make(map[string]int64)
|
||||
}
|
||||
for id := range s.Values {
|
||||
m, found := catalog.Find(id)
|
||||
if !found || (m.Season && s.SeasonStart != season.Unix()) || (!m.Season && s.DayStart != day.Unix()) {
|
||||
delete(s.Values, id)
|
||||
}
|
||||
}
|
||||
for key, at := range s.Events {
|
||||
if at < season.Unix() {
|
||||
delete(s.Events, key)
|
||||
}
|
||||
}
|
||||
s.DayStart, s.SeasonStart = day.Unix(), season.Unix()
|
||||
}
|
||||
|
||||
func warMissionScope(characterID string) string {
|
||||
return "character:" + characterID + ":guild_war_missions:v1"
|
||||
}
|
||||
func LoadWarMissions(ctx context.Context, repo dnfrepo.SettingsRepository, characterID string, catalog *WarMissionCatalog, now time.Time) (WarMissionState, error) {
|
||||
var s WarMissionState
|
||||
if repo == nil || characterID == "" || catalog == nil {
|
||||
return s, ErrRepositoryUnavailable
|
||||
}
|
||||
record, found, err := repo.Load(ctx, warMissionScope(characterID))
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
if found && record.Values["state"] != "" {
|
||||
if err = json.Unmarshal([]byte(record.Values["state"]), &s); err != nil {
|
||||
return s, err
|
||||
}
|
||||
}
|
||||
s.Reset(now, catalog)
|
||||
return s, nil
|
||||
}
|
||||
func SaveWarMissions(ctx context.Context, repo dnfrepo.SettingsRepository, characterID string, s WarMissionState, now time.Time) error {
|
||||
if repo == nil || characterID == "" {
|
||||
return ErrRepositoryUnavailable
|
||||
}
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return repo.Save(ctx, dnfrepo.SettingsRecord{Scope: warMissionScope(characterID), Values: map[string]string{"state": string(b)}, UpdatedAt: now})
|
||||
}
|
||||
|
||||
// Observe is called only for an accepted server-owned battle entry or clear.
|
||||
// Its receipt covers retries, including a retry after the daily reset.
|
||||
func (s *WarMissionState) Observe(catalog *WarMissionCatalog, kind string, dungeon uint32, key string, occurredAt, now time.Time) error {
|
||||
if catalog == nil || key == "" || (kind != "join" && kind != "clear") || occurredAt.After(now) {
|
||||
return ErrInvalidContent
|
||||
}
|
||||
s.Reset(now, catalog)
|
||||
if occurredAt.Unix() < s.SeasonStart {
|
||||
return ErrInvalidContent
|
||||
}
|
||||
key = kind + ":" + key
|
||||
if _, found := s.Events[key]; found {
|
||||
return nil
|
||||
}
|
||||
s.Events[key] = occurredAt.Unix()
|
||||
for _, m := range catalog.Missions {
|
||||
if m.Condition != kind || (!m.Season && occurredAt.Unix() < s.DayStart) {
|
||||
continue
|
||||
}
|
||||
if len(m.Dungeons) > 0 {
|
||||
matched := false
|
||||
for _, id := range m.Dungeons {
|
||||
matched = matched || id == dungeon
|
||||
}
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
}
|
||||
v := s.Values[m.ID]
|
||||
if v.Progress < m.Required {
|
||||
v.Progress++
|
||||
}
|
||||
s.Values[m.ID] = v
|
||||
}
|
||||
s.refreshTitles(catalog)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WarMissionState) refreshTitles(catalog *WarMissionCatalog) {
|
||||
for _, title := range catalog.Missions {
|
||||
if title.Condition != "mission" {
|
||||
continue
|
||||
}
|
||||
var completed uint32
|
||||
for _, m := range catalog.Missions {
|
||||
if m.Season == title.Season && m.Condition != "mission" && s.Values[m.ID].Progress >= m.Required {
|
||||
completed++
|
||||
}
|
||||
}
|
||||
v := s.Values[title.ID]
|
||||
v.Progress = min(completed, title.Required)
|
||||
s.Values[title.ID] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Claim changes the receipt in memory. The caller commits it with all item
|
||||
// grants in one repository transaction. An already claimed request is a no-op.
|
||||
func (s *WarMissionState) Claim(catalog *WarMissionCatalog, id uint32, now time.Time) ([]WarMissionReward, error) {
|
||||
s.Reset(now, catalog)
|
||||
m, found := catalog.Find(id)
|
||||
if !found {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
v := s.Values[id]
|
||||
if v.Claimed {
|
||||
return nil, nil
|
||||
}
|
||||
if v.Progress < m.Required {
|
||||
return nil, ErrWarMissionIncomplete
|
||||
}
|
||||
v.Claimed = true
|
||||
s.Values[id] = v
|
||||
return append([]WarMissionReward(nil), m.Rewards...), nil
|
||||
}
|
||||
|
||||
// ExperienceRanks uses competition ranking: 1, 1, 3 for tied experience.
|
||||
// It derives ranks from all persisted guilds, including offline guilds.
|
||||
func (r Registry) ExperienceRanks() map[uint32]uint32 {
|
||||
list := make([]*Guild, 0, len(r.Guilds))
|
||||
for _, g := range r.Guilds {
|
||||
if g != nil && g.ID != 0 {
|
||||
list = append(list, g)
|
||||
}
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
if list[i].Experience != list[j].Experience {
|
||||
return list[i].Experience > list[j].Experience
|
||||
}
|
||||
return list[i].ID < list[j].ID
|
||||
})
|
||||
ranks := make(map[uint32]uint32, len(list))
|
||||
var rank uint32
|
||||
for i, g := range list {
|
||||
if i == 0 || g.Experience != list[i-1].Experience {
|
||||
rank = uint32(i + 1)
|
||||
}
|
||||
ranks[g.ID] = rank
|
||||
}
|
||||
return ranks
|
||||
}
|
||||
|
||||
func invalidWarMission(id uint32) error {
|
||||
return fmt.Errorf("guild war mission %d: %w", id, ErrInvalidContent)
|
||||
}
|
||||
Reference in new issue
Block a user