合并本地领先源码:旧UI、GM疲劳燃烧、白金徽章孔、宠物修复、支援兵普攻开关
This commit is contained in:
1 parent
f2debaf9bf
commit
70b3c5a6dc
57 files changed
+4256
-30
No files matched your search
@@ -0,0 +1,268 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dnfcompanion "longheng.io/server/internal/modules/dnf/companion"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
// The teammate squad is the default support-soldier entry shape. It can be
|
||||
// disabled per deployment with DNF_COMPANION_TEAM_MODE=0; the GM settings stay
|
||||
// authoritative for which characters are preferred, but the server fills the
|
||||
// remaining slots from the same account at dungeon entry.
|
||||
const currentDungeonCompanionTeamModeEnv = "DNF_COMPANION_TEAM_MODE"
|
||||
|
||||
const currentDungeonCompanionTeamRole = "teammate"
|
||||
|
||||
func currentDungeonCompanionTeamModeEnabled() bool {
|
||||
switch strings.ToLower(strings.TrimSpace(os.Getenv(currentDungeonCompanionTeamModeEnv))) {
|
||||
case "0", "off", "false", "no":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// autoMatchCurrentDungeonCompanionSources expands the operator's saved
|
||||
// selection into a frozen teammate roster for this dungeon.
|
||||
//
|
||||
// This is a read-only matchmaking step: no source character is modified and no
|
||||
// session is created for it. The actual snapshot/AIC/projection pipeline still
|
||||
// runs per selected source exactly as before, so a bad or deleted character
|
||||
// fails on its own row instead of aborting the whole team.
|
||||
func (s *Service) autoMatchCurrentDungeonCompanionSources(
|
||||
ctx context.Context,
|
||||
repos dnfrepo.Group,
|
||||
accountID string,
|
||||
ownerCharacterID uint16,
|
||||
configured []string,
|
||||
) ([]string, []dnfcompanion.TeamSkip, bool) {
|
||||
if s == nil || repos.Character == nil || strings.TrimSpace(accountID) == "" ||
|
||||
!currentDungeonCompanionTeamModeEnabled() {
|
||||
return configured, nil, false
|
||||
}
|
||||
records, err := repos.Character.ListByAccount(ctx, accountID, dnfrepo.MaxCharacterSlotLimit)
|
||||
if err != nil {
|
||||
s.logPacketEvent("dnf-companion-team-match-deferred",
|
||||
"owner_character_id", ownerCharacterID,
|
||||
"reason", "character_list_unavailable",
|
||||
"error", err)
|
||||
return configured, nil, false
|
||||
}
|
||||
ownerLevel := 0
|
||||
if owner, ownerFound, ownerErr := repos.Character.Load(ctx, strconv.FormatUint(uint64(ownerCharacterID), 10)); ownerErr == nil && ownerFound {
|
||||
ownerLevel = owner.Level
|
||||
}
|
||||
levelFloor := ownerLevel - 10
|
||||
if levelFloor < 1 {
|
||||
levelFloor = 1
|
||||
}
|
||||
candidates := make([]dnfcompanion.TeamCandidate, 0, len(records))
|
||||
for _, record := range records {
|
||||
id := numericCharacterID(record)
|
||||
if id <= 0 || id >= int(^uint16(0)) {
|
||||
continue
|
||||
}
|
||||
if ownerLevel > 0 && record.Level > 0 && record.Level < levelFloor {
|
||||
// A level-21 character in a level-90 party's dungeon entry preflight
|
||||
// makes the whole party fail the level gate. Do not auto-fill with a
|
||||
// character the dungeon will reject.
|
||||
s.logPacketEvent("dnf-companion-team-match-level-skipped",
|
||||
"owner_character_id", ownerCharacterID,
|
||||
"candidate_character_id", id,
|
||||
"candidate_level", record.Level,
|
||||
"owner_level", ownerLevel,
|
||||
"level_floor", levelFloor)
|
||||
continue
|
||||
}
|
||||
session, online := s.onlineGameSession(uint16(id))
|
||||
if online && session != nil && session.companionHeadless {
|
||||
// Our own headless presence is not a competing real player. Without
|
||||
// this, every later auto-match (preflight/dungeon entry) excludes
|
||||
// the volunteers it just logged in.
|
||||
online = false
|
||||
}
|
||||
candidates = append(candidates, dnfcompanion.TeamCandidate{
|
||||
CharacterID: strconv.Itoa(id),
|
||||
Level: record.Level,
|
||||
Job: strings.TrimSpace(record.Job),
|
||||
Slot: record.Slot,
|
||||
Online: online,
|
||||
Deleted: record.Stats["delete_flag"] != 0,
|
||||
})
|
||||
}
|
||||
match := dnfcompanion.MatchTeam(
|
||||
strconv.FormatUint(uint64(ownerCharacterID), 10),
|
||||
configured,
|
||||
candidates,
|
||||
dnfcompanion.TeamMaxCompanions,
|
||||
)
|
||||
skipped := make([]string, 0, len(match.Skipped))
|
||||
for _, entry := range match.Skipped {
|
||||
if len(skipped) >= 16 {
|
||||
break
|
||||
}
|
||||
skipped = append(skipped, entry.CharacterID+":"+entry.Reason)
|
||||
}
|
||||
s.logPacketEvent("dnf-companion-team-match",
|
||||
"owner_character_id", ownerCharacterID,
|
||||
"configured", strings.Join(configured, ","),
|
||||
"selected", strings.Join(match.Selected, ","),
|
||||
"selected_count", len(match.Selected),
|
||||
"skipped", strings.Join(skipped, ","),
|
||||
"limit", dnfcompanion.TeamMaxCompanions)
|
||||
return match.Selected, match.Skipped, true
|
||||
}
|
||||
|
||||
func currentDungeonCompanionTeamSourceIDs(run *currentDungeonCompanionRun) []uint16 {
|
||||
if run == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]uint16, 0, len(run.Sources))
|
||||
for _, entry := range run.Sources {
|
||||
if entry.Ready {
|
||||
out = append(out, entry.SourceCharacterID)
|
||||
}
|
||||
}
|
||||
if len(out) == 0 && run.Ready && run.SourceCharacterID != 0 {
|
||||
out = append(out, run.SourceCharacterID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func currentDungeonCompanionTeamRevision(run *currentDungeonCompanionRun) uint64 {
|
||||
if run == nil {
|
||||
return 1
|
||||
}
|
||||
if len(run.Body) >= currentDungeonCompanionProjectionHeaderSize {
|
||||
if revision := binary.LittleEndian.Uint64(run.Body[8:16]); revision != 0 {
|
||||
return revision
|
||||
}
|
||||
}
|
||||
for _, entry := range run.Sources {
|
||||
if len(entry.Body) >= currentDungeonCompanionProjectionHeaderSize {
|
||||
if revision := binary.LittleEndian.Uint64(entry.Body[8:16]); revision != 0 {
|
||||
return revision
|
||||
}
|
||||
}
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func (s *Service) markCurrentDungeonCompanionTeamOnline(
|
||||
session *gameSession,
|
||||
run *currentDungeonCompanionRun,
|
||||
source string,
|
||||
) {
|
||||
if s == nil || run == nil || !run.Ready {
|
||||
return
|
||||
}
|
||||
run.Online = true
|
||||
run.OnlineAt = s.gameplayNow()
|
||||
run.OfflineAt = time.Time{}
|
||||
run.OfflineReason = ""
|
||||
run.LogoutSent = false
|
||||
s.logGameEvent(session, "game-dungeon-companion-team-online",
|
||||
"source", source,
|
||||
"team_mode", run.TeamMode,
|
||||
"team_leader_id", run.TeamLeaderID,
|
||||
"team_size", run.TeamSize,
|
||||
"ready_count", currentDungeonCompanionReadyCount(run),
|
||||
"source_character_ids", uint16ListLog(currentDungeonCompanionTeamSourceIDs(run)),
|
||||
"online_at", run.OnlineAt)
|
||||
}
|
||||
|
||||
// sendCurrentDungeonCompanionTeamOffline publishes the explicit present=0
|
||||
// projection. The client clears every queued/live support actor on that body,
|
||||
// so one packet is enough for the whole squad and it is safe to call from every
|
||||
// dungeon boundary: the run's LogoutSent flag makes it idempotent.
|
||||
func (s *Service) sendCurrentDungeonCompanionTeamOffline(
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
reason string,
|
||||
) {
|
||||
if s == nil || session == nil || runtime == nil {
|
||||
return
|
||||
}
|
||||
run := runtime.Companion
|
||||
if run == nil || !run.Ready || run.LogoutSent {
|
||||
return
|
||||
}
|
||||
run.LogoutSent = true
|
||||
run.Online = false
|
||||
run.OfflineAt = s.gameplayNow()
|
||||
run.OfflineReason = strings.TrimSpace(reason)
|
||||
// The dungeon party frame goes away; the synthetic friend sessions do not.
|
||||
s.clearCurrentDungeonCompanionNativeParty(session, run)
|
||||
ids := currentDungeonCompanionTeamSourceIDs(run)
|
||||
body := buildCurrentDungeonCompanionOfflineBody(currentDungeonCompanionTeamRevision(run))
|
||||
s.logGameEvent(session, "game-dungeon-companion-team-offline",
|
||||
"reason", run.OfflineReason,
|
||||
"team_mode", run.TeamMode,
|
||||
"team_leader_id", run.TeamLeaderID,
|
||||
"team_size", run.TeamSize,
|
||||
"offline_count", len(ids),
|
||||
"source_character_ids", uint16ListLog(ids),
|
||||
"offline_at", run.OfflineAt,
|
||||
"private_opcode", currentDungeonCompanionProjectionOpcode,
|
||||
"body_len", len(body))
|
||||
if err := s.sendGameUpperRawClass(
|
||||
session,
|
||||
currentDungeonCompanionProjectionOpcode,
|
||||
body,
|
||||
0,
|
||||
); err != nil {
|
||||
s.logGameEvent(session, "game-dungeon-companion-team-offline-send-failed",
|
||||
"reason", run.OfflineReason,
|
||||
"error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) sendCurrentDungeonCompanionTeamOfflineCurrent(
|
||||
session *gameSession,
|
||||
reason string,
|
||||
) {
|
||||
if s == nil || session == nil {
|
||||
return
|
||||
}
|
||||
session.dungeon.mu.Lock()
|
||||
runtime := session.dungeon.runtime
|
||||
session.dungeon.mu.Unlock()
|
||||
s.sendCurrentDungeonCompanionTeamOffline(session, runtime, reason)
|
||||
}
|
||||
|
||||
func uint16ListLog(values []uint16) string {
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
var builder strings.Builder
|
||||
for index, value := range values {
|
||||
if index != 0 {
|
||||
builder.WriteByte(',')
|
||||
}
|
||||
builder.WriteString(strconv.Itoa(int(value)))
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// runtimeDungeonCompanionCurrent is the compare step used by exit paths that
|
||||
// opportunistically publish the logout before taking session.dungeon.mu. The
|
||||
// final authority remains retireCurrentDungeonRuntimeLocked; this only avoids
|
||||
// sending a stale run's packet when a newer dungeon already replaced it.
|
||||
func (s *Service) runtimeDungeonCompanionCurrent(
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
) bool {
|
||||
if s == nil || session == nil || runtime == nil {
|
||||
return false
|
||||
}
|
||||
session.dungeon.mu.Lock()
|
||||
defer session.dungeon.mu.Unlock()
|
||||
return session.dungeon.runtime == runtime
|
||||
}
|
||||
Reference in new issue
Block a user