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,282 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/raid"
|
||||
)
|
||||
|
||||
// Luke phase-one generators ("破坏目标 x/5").
|
||||
//
|
||||
// Each phase-one sanctum names its own counter in [DESTROY OBJECT INFO]
|
||||
// ([BIRTH SANCTUM OBJECT] = symbol 117, extinction 118, spread 119) and caps it
|
||||
// at [DEFAULT OBJECT COUNT] 5. luke.etc only zeroes them ([1PHASE INIT]) and
|
||||
// resets one on giving up its sanctum ([RESET SANCTUM OBJECT]); [2PHASE INIT]
|
||||
// leaves them alone. What reads them is [1PHASE OBJ DESTROY]: their total picks
|
||||
// the tier stored in [ENERGY CONTROL OPEN] (133), and the war board draws each
|
||||
// sanctum's five target icons lit for cap - symbol (DNF.exe sub_10D84D0: cap =
|
||||
// sub_10D8450 clamped to 5, value = the raid symbol sub_17C2CF0 names). A fresh
|
||||
// sanctum therefore shows 5/5 and loses one icon per destroyed generator.
|
||||
//
|
||||
// The generator is the sanctum's main machine, not the batteries or dry cells
|
||||
// around it (湮灭之圣所炼金炉 = sanctum_of_extinction/mainmchine_raid.obj). Its
|
||||
// PVF is [hp destroy] 0: at HP <= 1 its basic action switches to the destroy
|
||||
// action, whose only behaviour is [RAID SET SYMBOL] <sanctum symbol> [+] 1:
|
||||
//
|
||||
// birth luke_mainmchine_destroy_raid.act / _mistral_raid.act 117 + 1
|
||||
// extinction mainmchine_destroy_raid.act 118 + 1
|
||||
// spread mainmchine_destroy.act / censor_machine_broken_front_raid.act 119 + 1
|
||||
//
|
||||
// The client turns that into C2S op662 {u32 symbol, u32 operand, u8 operation}
|
||||
// (sub_17B1600). No op39 is sent for the machine. The script runs on every
|
||||
// member's client, so one machine arrives once per member standing in the
|
||||
// room; a room is credited with the most reports any single member made there.
|
||||
|
||||
type runtimeRaidLukeGeneratorReportKey struct {
|
||||
MapID int
|
||||
LifecycleToken uint64
|
||||
Room runtimeDungeonRoomKey
|
||||
}
|
||||
|
||||
type runtimeRaidLukeGeneratorRoomReports struct {
|
||||
ByMember map[uint16]uint32
|
||||
Credited uint32
|
||||
}
|
||||
|
||||
func cloneRuntimeRaidLukeGeneratorReports(
|
||||
reports map[runtimeRaidLukeGeneratorReportKey]runtimeRaidLukeGeneratorRoomReports,
|
||||
) map[runtimeRaidLukeGeneratorReportKey]runtimeRaidLukeGeneratorRoomReports {
|
||||
if reports == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[runtimeRaidLukeGeneratorReportKey]runtimeRaidLukeGeneratorRoomReports, len(reports))
|
||||
for key, room := range reports {
|
||||
byMember := make(map[uint16]uint32, len(room.ByMember))
|
||||
for member, count := range room.ByMember {
|
||||
byMember[member] = count
|
||||
}
|
||||
out[key] = runtimeRaidLukeGeneratorRoomReports{ByMember: byMember, Credited: room.Credited}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func runtimeRaidLukeSanctumObjectSymbol(mapID int) (uint32, bool) {
|
||||
switch mapID {
|
||||
case lukeBirthSanctumMapID:
|
||||
return lukeSymbolBirthSanctumObject, true
|
||||
case lukeExtinctionSanctumMapID:
|
||||
return lukeSymbolExtinctionSanctumObject, true
|
||||
case lukeSpreadSanctumMapID:
|
||||
return lukeSymbolSpreadSanctumObject, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// runtimeRaidLukeEnergyControlTierLocked is the [1PHASE OBJ DESTROY] tier of
|
||||
// the three sanctum counters. A raid without its Luke config falls back to tier
|
||||
// 0, the shortest respawn. Caller holds raidMu.
|
||||
func runtimeRaidLukeEnergyControlTierLocked(state *runtimeRaidState) uint32 {
|
||||
if state == nil || state.LukeConfig == nil {
|
||||
return 0
|
||||
}
|
||||
total := runtimeRaidLukeSymbolLocked(state, lukeSymbolBirthSanctumObject) +
|
||||
runtimeRaidLukeSymbolLocked(state, lukeSymbolExtinctionSanctumObject) +
|
||||
runtimeRaidLukeSymbolLocked(state, lukeSymbolSpreadSanctumObject)
|
||||
tier, ok := state.LukeConfig.EnergyControlTier(int(total))
|
||||
if !ok || tier < 0 {
|
||||
return 0
|
||||
}
|
||||
return uint32(tier)
|
||||
}
|
||||
|
||||
type runtimeRaidLukeGeneratorCount struct {
|
||||
RaidKey uint32
|
||||
Phase byte
|
||||
Symbol uint32
|
||||
Before uint32
|
||||
After uint32
|
||||
Cap int
|
||||
}
|
||||
|
||||
// countRuntimeRaidLukeGeneratorLocked credits one destroyed generator to the
|
||||
// member's sanctum counter, saturating at [DEFAULT OBJECT COUNT]. Caller holds
|
||||
// raidMu.
|
||||
func countRuntimeRaidLukeGeneratorLocked(state *runtimeRaidState, mapID int) (runtimeRaidLukeGeneratorCount, bool) {
|
||||
if state == nil || state.TailFlag == 1 || !state.Started ||
|
||||
runtimeRaidTeamGraphOwner(*state) != runtimeRaidOwnerLuke ||
|
||||
state.Lifecycle.Status != runtimeRaidLifecyclePhaseActive ||
|
||||
state.Lifecycle.Phase != runtimeRaidDungeonPhaseOne || state.LukeConfig == nil {
|
||||
return runtimeRaidLukeGeneratorCount{}, false
|
||||
}
|
||||
symbol, ok := runtimeRaidLukeSanctumObjectSymbol(mapID)
|
||||
if !ok {
|
||||
return runtimeRaidLukeGeneratorCount{}, false
|
||||
}
|
||||
capacity, ok := state.LukeConfig.SanctumObjectCount(uint32(mapID))
|
||||
if !ok || capacity <= 0 {
|
||||
return runtimeRaidLukeGeneratorCount{}, false
|
||||
}
|
||||
result := runtimeRaidLukeGeneratorCount{
|
||||
RaidKey: state.RaidKey,
|
||||
Phase: state.Lifecycle.Phase,
|
||||
Symbol: symbol,
|
||||
Before: runtimeRaidLukeSymbolLocked(state, symbol),
|
||||
Cap: capacity,
|
||||
}
|
||||
result.After = result.Before
|
||||
if int(result.Before) >= capacity {
|
||||
return result, false
|
||||
}
|
||||
result.After = result.Before + 1
|
||||
setRuntimeRaidLukeSymbolLocked(state, symbol, result.After)
|
||||
return result, true
|
||||
}
|
||||
|
||||
// recordRuntimeRaidLukeGeneratorReportLocked books one member's op662 for a
|
||||
// room and reports whether it is a machine no other member has reported yet.
|
||||
// Caller holds raidMu.
|
||||
func recordRuntimeRaidLukeGeneratorReportLocked(
|
||||
state *runtimeRaidState,
|
||||
key runtimeRaidLukeGeneratorReportKey,
|
||||
characterID uint16,
|
||||
) (runtimeRaidLukeGeneratorRoomReports, bool) {
|
||||
if state.Lifecycle.LukeGeneratorReports == nil {
|
||||
state.Lifecycle.LukeGeneratorReports = make(map[runtimeRaidLukeGeneratorReportKey]runtimeRaidLukeGeneratorRoomReports)
|
||||
}
|
||||
room := state.Lifecycle.LukeGeneratorReports[key]
|
||||
if room.ByMember == nil {
|
||||
room.ByMember = make(map[uint16]uint32)
|
||||
}
|
||||
room.ByMember[characterID]++
|
||||
fresh := room.ByMember[characterID] > room.Credited
|
||||
if fresh {
|
||||
room.Credited = room.ByMember[characterID]
|
||||
}
|
||||
state.Lifecycle.LukeGeneratorReports[key] = room
|
||||
return room, fresh
|
||||
}
|
||||
|
||||
// runtimeRaidCurrentRaidDungeonRoom is runtimeRaidCurrentRaidDungeonRun plus the
|
||||
// room the session stands in.
|
||||
//
|
||||
// The binding's token is the SQUAD's run token, never the adopting member's own
|
||||
// (see the adoption branch of beginRuntimeRaidDungeon), so it must NOT be compared
|
||||
// with the session's local token here -- exactly the trap the Anton twin
|
||||
// (runtimeRaidCurrentRaidDungeonRun) documents. Upstream's Anton helper had that
|
||||
// comparison removed on 2026-09-19 (raid 1507338: every shield report but the
|
||||
// opener's was silently dropped) while this Luke helper kept it, and the port
|
||||
// brought the bug across:
|
||||
//
|
||||
// live 2026-09-29 09:21-09:23Z, raid 1835009: char 22 opened the 湮灭之圣所 run
|
||||
// (binding token 7), char 1 walked in behind it (its own token 8, per
|
||||
// game-raid-dungeon-lifecycle-joined-clear). The machine's hand-written
|
||||
// [RAID SET SYMBOL] 118 [+] 1 arrived as op662 {118, 1, add} and was declined
|
||||
// here, fell through to the Anton barrier owner and was logged as
|
||||
// game-raid-barrier-request-rejected reason=not_inside_energy_or_hatchery_run.
|
||||
// 破坏目标 stayed 5/5 for the whole run while the same reports from the run's
|
||||
// opener had counted fine earlier that day.
|
||||
//
|
||||
// What a report has to prove is that this member stands inside the gate its
|
||||
// binding names; the binding's token stays the caller's per-run dedupe key.
|
||||
func runtimeRaidCurrentRaidDungeonRoom(session *gameSession) (runtimeRaidDungeonBinding, runtimeDungeonRoomKey, bool) {
|
||||
if session == nil {
|
||||
return runtimeRaidDungeonBinding{}, runtimeDungeonRoomKey{}, false
|
||||
}
|
||||
session.dungeon.mu.Lock()
|
||||
defer session.dungeon.mu.Unlock()
|
||||
runtime := session.dungeon.runtime
|
||||
if runtime == nil || runtime.Room == nil || runtime.raidDungeon.RaidKey == 0 ||
|
||||
runtime.raidDungeon.LifecycleToken == 0 ||
|
||||
!dungeonRuntimeOwnsCharacter(runtime, session.selectedCharacterID) {
|
||||
return runtimeRaidDungeonBinding{}, runtimeDungeonRoomKey{}, false
|
||||
}
|
||||
runtime.Room.mu.RLock()
|
||||
room := runtimeDungeonRoomKey{X: runtime.Room.coordinate.X, Y: runtime.Room.coordinate.Y, MapID: runtime.Room.mapID}
|
||||
runtime.Room.mu.RUnlock()
|
||||
return runtime.raidDungeon, room, true
|
||||
}
|
||||
|
||||
// applyRuntimeRaidLukeGeneratorReport owns an op662 sent from inside a Luke
|
||||
// phase-one sanctum for that sanctum's own counter. It returns false for any
|
||||
// other op662 so the phase-two shield owner still sees it.
|
||||
func (s *Service) applyRuntimeRaidLukeGeneratorReport(session *gameSession, req raid.SetSymbolRequest) bool {
|
||||
characterID := selectedCharacterID(session)
|
||||
if s == nil || characterID == 0 {
|
||||
return false
|
||||
}
|
||||
run, room, inRun := runtimeRaidCurrentRaidDungeonRoom(session)
|
||||
if !inRun {
|
||||
return false
|
||||
}
|
||||
symbol, sanctum := runtimeRaidLukeSanctumObjectSymbol(run.MapID)
|
||||
if !sanctum || req.SourceValue != symbol {
|
||||
return false
|
||||
}
|
||||
if req.Symbol != runtimeRaidSymbolOperationAdd || req.TargetValue != 1 || run.Phase != runtimeRaidDungeonPhaseOne {
|
||||
s.logGameEvent(session, "game-raid-luke-generator-report-rejected",
|
||||
"char_id", characterID, "sanctum", run.MapID, "symbol", req.SourceValue,
|
||||
"operand", req.TargetValue, "operation", req.Symbol, "run_phase", run.Phase,
|
||||
"reason", "not_a_single_generator_increment")
|
||||
return true
|
||||
}
|
||||
|
||||
s.raidMu.Lock()
|
||||
state := s.runtimeRaidByMemberLocked(characterID)
|
||||
if state == nil || state.RaidKey != run.RaidKey {
|
||||
s.raidMu.Unlock()
|
||||
s.logGameEvent(session, "game-raid-luke-generator-report-rejected",
|
||||
"char_id", characterID, "sanctum", run.MapID, "reason", "raid_not_found")
|
||||
return true
|
||||
}
|
||||
key := runtimeRaidLukeGeneratorReportKey{MapID: run.MapID, LifecycleToken: run.LifecycleToken, Room: room}
|
||||
reports, fresh := recordRuntimeRaidLukeGeneratorReportLocked(state, key, characterID)
|
||||
var count runtimeRaidLukeGeneratorCount
|
||||
counted := false
|
||||
if fresh {
|
||||
count, counted = countRuntimeRaidLukeGeneratorLocked(state, run.MapID)
|
||||
}
|
||||
s.raidMu.Unlock()
|
||||
|
||||
s.logGameEvent(session, "game-raid-luke-generator-reported",
|
||||
"char_id", characterID,
|
||||
"raid_key", run.RaidKey,
|
||||
"sanctum", run.MapID,
|
||||
"room", fmt.Sprintf("%d:%d", room.X, room.Y),
|
||||
"map_id", room.MapID,
|
||||
"member_reports", reports.ByMember[characterID],
|
||||
"room_credited", reports.Credited,
|
||||
"fresh", fresh,
|
||||
"symbol", symbol,
|
||||
"count_before", count.Before,
|
||||
"count_after", count.After,
|
||||
"cap", count.Cap,
|
||||
"counted", counted)
|
||||
if counted {
|
||||
s.scheduleRuntimeRaidLukeProgressPublish(count.RaidKey, count.Phase, fmt.Sprintf("generator:%d:%d", run.MapID, count.After))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// scheduleRuntimeRaidLukeProgressPublish publishes the phase snapshot once the
|
||||
// caller's locks are gone (see scheduleRuntimeRaidLukeEntryProgressPublish).
|
||||
func (s *Service) scheduleRuntimeRaidLukeProgressPublish(raidKey uint32, phase byte, reason string) {
|
||||
if s == nil || raidKey == 0 {
|
||||
return
|
||||
}
|
||||
queue := s.ensureGameplayTimeQueue()
|
||||
if queue == nil {
|
||||
return
|
||||
}
|
||||
name := fmt.Sprintf("raid-luke-progress:%d:%s", raidKey, reason)
|
||||
if err := queue.ScheduleAfter(name, 0, func(time.Time) {
|
||||
if err := s.publishRuntimeRaidObjectiveProgress(raidKey, phase); err != nil {
|
||||
s.logPacketEvent("game-raid-luke-progress-publish-failed",
|
||||
"raid_key", raidKey, "reason", reason, "error", err)
|
||||
}
|
||||
}); err != nil {
|
||||
s.logPacketEvent("game-raid-luke-progress-schedule-failed",
|
||||
"raid_key", raidKey, "reason", reason, "error", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user