Files
90CNServer/server/internal/services/dnfbridge/daily_delete_item.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

187 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package dnfbridge
import (
"context"
"strconv"
"time"
"longheng.io/server/internal/modules/dnf/premium"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
// PVF `[daily delete item] 1` —— 每日重置时删除的物品。
//
// # 为什么不能复用 expire_at
//
// 本服**有意屏蔽了物品期限**:cmd/scratch/pvfremoveitemexpiry 从运行时 PVF 摘掉
// `[expiration date]` / `[usable datetime]` / `[usable period]` /
// `[usable period after unsealing]`,cmd/scratch/removeitemexpirydb 清掉库里的
// 期限字段。所以 expire_at 这条通道是被主动关掉的,往它上面盖戳会在下一次跑那两个
// 工具时被一起清掉。
//
// `[daily delete item]` **不在那两个工具的剥离名单里**,在运行时 PVF 里仍然有效 ——
// 客户端把它渲染成"期限 1 天",而服务端此前完全没解析过这个字段(全仓库 grep
// "daily delete" 零命中),于是这类物品永远不会消失。玩家实测:公会祝福礼盒
// (490001382)与魔界抗疲劳秘药(490001383)显示 1 天期限却一直留在背包里。
//
// 所以这里用**独立的状态位**,不碰 expire_at:把物品被发放(或首次被看见)的服务日
// 记在 stack.Extra 里,跨过服务日边界就删。这样期限屏蔽照旧、每日删除照常生效,
// 两者互不影响。
//
// # 边界
//
// 服务日边界是北京 06:00,复用 premium.ServiceDayID(它把 unix 秒 +2h 再按 24h 分桶,
// 因为 06:00 UTC+8 就是前一civil日的 22:00 UTC)。这和库里现有期限值全都落在
// `22:00:00` 是同一个边界。
//
// # 存量
//
// 已经在背包里、没有戳的旧物品**不当场没收**(运营决定 2026-09-16):扫描时补盖成
// 当天,于是它们在下一个 06:00 消失。
const (
// currentDailyDeleteItemStateKey 存放该物品实例被发放/首次看见的服务日 ID。
currentDailyDeleteItemStateKey = "daily_delete_day"
)
// currentDailyDeleteServiceDay 是当前服务日 ID。
func (s *Service) currentDailyDeleteServiceDay(now time.Time) int64 {
if now.IsZero() {
now = s.gameplayNow()
}
return premium.ServiceDayID(now.UTC())
}
// stampCurrentDailyDeleteItem 在发放时给每日删除物品打上当前服务日。
// 非每日删除物品原样返回,调用方无需先判断。
func stampCurrentDailyDeleteItem(
stack dnfrepo.ItemStack,
definition dungeonDropItemDefinition,
serviceDay int64,
) (dnfrepo.ItemStack, bool) {
if !definition.DailyDeleteItem {
return stack, false
}
value := strconv.FormatInt(serviceDay, 10)
if stack.Extra != nil && stack.Extra[currentDailyDeleteItemStateKey] == value {
return stack, false
}
if stack.Extra == nil {
stack.Extra = make(map[string]string, 1)
}
stack.Extra[currentDailyDeleteItemStateKey] = value
return stack, true
}
// currentDailyDeleteItemDue 判断一个已打戳的物品是不是该删了。
// 没有戳的返回 (false, false):调用方应当补盖,而不是当场删除。
func currentDailyDeleteItemDue(stack dnfrepo.ItemStack, serviceDay int64) (due bool, stamped bool) {
raw, ok := stack.Extra[currentDailyDeleteItemStateKey]
if !ok {
return false, false
}
stored, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
// 戳坏了就当没有:补盖一次,下一个边界再删。总比凭一个读不懂的值没收东西好。
return false, false
}
return serviceDay > stored, true
}
// sweepSelectedDailyDeleteItems 在选人/背包 bootstrap 时跑一次:跨过服务日边界的
// 每日删除物品就地删掉,没有戳的补盖成当天。
//
// 只解析**背包里实际存在的** item id 的定义(几十个),不扫全 PVF —— 公会合成那次
// 全量 ResolveItem 让第一次操作卡了 8.66 秒,这里不重蹈覆辙。
func (s *Service) sweepSelectedDailyDeleteItems(session *gameSession, source string) {
if s == nil || session == nil || session.selectedCharacterID == 0 {
return
}
items, err := s.currentPVFItemCatalog()
if err != nil {
return
}
repositories, ok := s.repositoryGroup()
if !ok || repositories.CharacterAssets == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
defer cancel()
characterID := strconv.Itoa(int(session.selectedCharacterID))
serviceDay := s.currentDailyDeleteServiceDay(s.gameplayNow())
now := s.gameplayNow().UTC()
var removed, stamped []string
err = repositories.CharacterAssets.WithinCharacterAssets(
ctx,
characterID,
func(
_ dnfrepo.CharacterRepository,
inventories dnfrepo.InventoryRepository,
_ dnfrepo.EquipmentRepository,
) error {
record, found, loadErr := inventories.Load(ctx, characterID)
if loadErr != nil || !found {
return loadErr
}
record = dnfrepo.CloneInventory(record)
if len(record.Slots) == 0 {
return nil
}
// item id -> 是否每日删除,按背包里出现过的 id 缓存,避免同一个 id
// 在多个格子里重复解析。
daily := make(map[int64]bool, 16)
changed := false
for key, stack := range record.Slots {
if stack.ItemID <= 0 || stack.ItemID > int64(^uint32(0)) {
continue
}
isDaily, seen := daily[stack.ItemID]
if !seen {
definition, defErr := items.ResolveItem(uint32(stack.ItemID))
isDaily = defErr == nil && definition.DailyDeleteItem
daily[stack.ItemID] = isDaily
}
if !isDaily {
continue
}
due, hasStamp := currentDailyDeleteItemDue(stack, serviceDay)
switch {
case due:
delete(record.Slots, key)
removed = append(removed, key)
changed = true
case !hasStamp:
// 存量补盖:不当场没收,下一个服务日边界才消失。
updated, dirty := stampCurrentDailyDeleteItem(stack, dungeonDropItemDefinition{DailyDeleteItem: true}, serviceDay)
if dirty {
record.Slots[key] = updated
stamped = append(stamped, key)
changed = true
}
}
}
if !changed {
return nil
}
record.UpdatedAt = now
return dnfrepo.SaveInventoryFields(ctx, inventories, record, dnfrepo.InventoryFieldSlots)
},
)
if err != nil {
s.logGameEvent(session, "game-daily-delete-item-sweep-failed",
"source", source, "char_id", session.selectedCharacterID, "reason", err)
return
}
if len(removed) == 0 && len(stamped) == 0 {
return
}
s.logGameEvent(session, "game-daily-delete-item-sweep-applied",
"source", source,
"char_id", session.selectedCharacterID,
"service_day", serviceDay,
"removed", len(removed),
"removed_keys", removed,
"stamped", len(stamped),
"stamped_keys", stamped)
}