Files
90CNServer/server/internal/services/dnfbridge/guild_management.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

597 lines
26 KiB
Go

package dnfbridge
import (
"encoding/binary"
"strconv"
"strings"
"time"
"longheng.io/server/internal/modules/dnf/dnfenum"
dnfguild "longheng.io/server/internal/modules/dnf/guild"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
// currentGuildMemberOfflineChannel is the channel byte sub_1D21FD0 turns into
// member+0x34 == -1 (01D223DE cmp cl,0xff / 01D223E3 mov [eax+0x34],-1). An
// earlier note here claimed the window renders no row in that state; the probe
// on 2026-09-08 disproved it -- both places the window compares that field feed
// a `setne` boolean into the row-render call. What actually distinguishes an
// offline member is the elapsed-offline u32; see
// currentGuildMemberOfflineSeconds. An online member must still never be
// encoded with this value, because it would report as offline.
const currentGuildMemberOfflineChannel = byte(0xff)
// currentGuildMemberUnrepresentedChannel marks "online, channel number does
// not fit the row's one-byte field". No advertised channel needs it any more:
// `[deathtower]` moved 508 -> 171, and `[pvp]` (501..507 -> 191..197) and
// `[fiendwar]` (261..263 -> 184..186) followed once the same one-byte limit
// turned out to break town co-presence too, not just this row.
// TestCurrentChannelCatalogFitsGuildMemberChannelByte pins that the exemption
// set is now empty. The guard stays as a protocol rail so a future
// out-of-range channel degrades to "online, channel unknown" instead of
// silently becoming offline. The value is deliberately outside the advertised
// directory so it can never be mistaken for a real channel the way a
// truncated 508 -> 252 was.
const currentGuildMemberUnrepresentedChannel = byte(0xfe)
// currentGuildMemberChannelByte encodes an online member's channel for a
// member-list row. The field is one byte and 0xff is the offline sentinel, so
// only 1..254 are expressible. Keeping the member visible matters more than
// the channel number, so an unrepresentable channel still reports online.
func currentGuildMemberChannelByte(channelID int) byte {
if channelID <= 0 || channelID > 254 {
return currentGuildMemberUnrepresentedChannel
}
if encoded := byte(channelID); encoded != currentGuildMemberOfflineChannel {
return encoded
}
return currentGuildMemberUnrepresentedChannel
}
func (s *Service) currentGuildOnlineChannels(guild *dnfguild.Guild, requester *gameSession) map[string]byte {
channels := make(map[string]byte)
if guild == nil {
return channels
}
for id := range guild.Members {
// The requester's own row previously bypassed the range check while
// every other row was dropped from the map, so a member above channel
// 254 was announced online to itself and offline to everyone else.
// One encoder now serves both.
identity, found := s.onlineGameSessionCharacterSnapshot(uint16(currentGuildNumericID(id)))
if found && identity.session.conn != nil && identity.session.channel.ID > 0 {
channels[id] = currentGuildMemberChannelByte(identity.session.channel.ID)
if channels[id] == currentGuildMemberUnrepresentedChannel {
s.logPacketEvent("game-guild-member-channel-unrepresented",
"char_id", id, "channel_id", identity.session.channel.ID)
}
}
if requester != nil && id == strconv.Itoa(int(requester.selectedCharacterID)) {
channels[id] = currentGuildMemberChannelByte(requester.channel.ID)
}
}
return channels
}
func (s *Service) sendCurrentGuildManagementState(session *gameSession, guild *dnfguild.Guild) error {
// Login requests op140 before actor creation; op127 dereferences that actor.
// The initial-town projection sends the latest state after the transition.
// The initial-town route cursor alone is not enough to express "the actor
// exists": a cross-town move (op1425 / town_move) commits its own scene
// transition and then resets the cursor to idle, so every later op70/op127
// for that session was dropped in silence. A committed town scene for the
// selected character is the same guarantee the cursor gives.
if ready, source := s.currentSceneTransitionReadyForState(session); !ready {
if !currentGuildTownActorCommitted(session) {
s.logGameEvent(session, "game-guild-management-state-deferred", "reason", source)
return nil
}
}
if err := s.writeCurrentGuildManagementState(session, guild); err != nil {
return err
}
// The local actor's insignia may have just changed. Peers that already built
// this actor keep the old decoration otherwise, because the aura rides the
// actor-appearance block rather than op70.
s.publishCurrentGuildAuraToTownPeers(session, guild)
return nil
}
// currentGuildTownActorCommitted reports whether the session already committed
// a town scene for its selected character, which is what op127 dereferences.
func currentGuildTownActorCommitted(session *gameSession) bool {
if session == nil || session.selectedCharacterID == 0 {
return false
}
session.townMu.Lock()
committed := session.townSceneReadyCharacterID == session.selectedCharacterID
session.townMu.Unlock()
return committed
}
// currentGuildTownActorCommittedNonBlocking is the peer-safe form of
// currentGuildTownActorCommitted; see
// currentSceneTransitionReadyForStateNonBlocking for why a peer read must not
// wait for that peer's town lock.
func currentGuildTownActorCommittedNonBlocking(session *gameSession) (committed bool, busy bool) {
if session == nil || session.selectedCharacterID == 0 {
return false, false
}
if !session.townMu.TryLock() {
return false, true
}
committed = session.townSceneReadyCharacterID == session.selectedCharacterID
session.townMu.Unlock()
return committed, false
}
// sendCurrentGuildManagementStateToPeer refreshes another online member's guild
// window without ever waiting for that member's town lock.
//
// The peer refresh runs on the event loop of the member whose online state just
// changed, and that member's own townMu is already held there:
// sendCurrentInitialTownRoute publishes presence from inside its lock
// (initial_town_route_progress.go). Reading the peer's town state with a blocking
// lock at that point inverts the lock order between two members logging in at the
// same time. Live incident 2026-09-22 20:06 -- the sessions of 1111 and 2222 (one
// guild, one second apart) each held their own townMu and waited for the other's:
//
// goroutine 934: handleDungeonTutorialFlag -> handleCurrentInitialTownProgress
// -> sendCurrentInitialTownRoute -> publishCurrentTownPresence ->
// publishCurrentSocialPresence -> publishCurrentGuildPresence ->
// sendCurrentGuildManagementState -> currentSceneTransitionReadyForState
// (initial_town_scene_ready.go:58, sync.Mutex.Lock, 6 minutes)
//
// Neither session processed another packet afterwards, so both clients stayed
// frozen in the scene they had just logged into, the door out of the Seria room
// included, and every later login that published into a wedged session wedged as
// well. The refresh is only a projection of the peer's own guild window, which
// the peer's own login route sends anyway, so a peer that is mid-transition is
// skipped with an event instead of being waited for.
func (s *Service) sendCurrentGuildManagementStateToPeer(session *gameSession, guild *dnfguild.Guild) error {
if session == nil {
return nil
}
ready, source, busy := s.currentSceneTransitionReadyForStateNonBlocking(session)
if busy {
s.logGameEvent(session, "game-guild-presence-refresh-deferred", "reason", source)
return nil
}
if !ready {
committed, busy := currentGuildTownActorCommittedNonBlocking(session)
if busy {
s.logGameEvent(session, "game-guild-presence-refresh-deferred", "reason", source)
return nil
}
if !committed {
s.logGameEvent(session, "game-guild-management-state-deferred", "reason", source)
return nil
}
}
return s.writeCurrentGuildManagementState(session, guild)
}
// The caller must have committed the selected actor and scene transition.
func (s *Service) writeCurrentGuildManagementState(session *gameSession, guild *dnfguild.Guild) error {
grade := byte(100) // sub_1D85270's native nonmember sentinel
if guild != nil {
if member, found := guild.Members[strconv.Itoa(int(session.selectedCharacterID))]; found {
grade = member.Grade
} else {
// A nonmember of the guild being projected must not receive op70 at
// all: its body carries the notice, the insignia and the grade
// table, none of which a nonmember may see. Upstream nils the guild
// here so that op70 and every projection below are skipped while
// op127 still delivers the 100 sentinel.
guild = nil
}
}
// A nonmember gets only the grade sentinel. op70's handler displays the
// notice once per client run gated on nothing but a global flag --
// 01D8B8A2 tests [0x519297C], sets it at 01D8B8BD, then formats the notice
// through string 0x1A46 and shows it (01D8B8D3..01D8B92D). It never checks
// whether the notice is empty or whether the viewer is in a guild, so the
// empty projection the login path sends for a guildless character popped a
// blank 公会公告 line on every login. Skipping op70 costs the "clear the
// previous character's guild fields on switch" side effect that the empty
// projection also had; op127's 100 sentinel plus the membership
// notification still tell the client it is not a member, and the notice is
// one-shot per client run anyway.
if guild != nil {
if err := s.sendGameUpperRawClass(session, 70, buildCurrentGuildManagementBody(guild, len(s.currentGuildOnlineChannels(guild, session)), s.gameplayNow(), grade), 0); err != nil {
return err
}
}
if err := s.sendGameUpperRawClass(session, 127, []byte{grade}, 0); err != nil {
return err
}
// Projections that must follow op127. op70 carries the durable guild fields
// but not the actor decorations or the HUD rows, so without these a member
// who changes guild state keeps the previous insignia, buffs and rank until
// a relog. Upstream also writes the 公会地下城 and 守护神 states here
// (writeCurrentGuildDungeonState / writeCurrentGuildGuardianState); those
// arrive with their own steps.
if err := s.writeCurrentGuildAuraState(session, session.selectedCharacterID, guild, currentTownActorOwnerContext(session)); err != nil {
return err
}
if err := s.writeCurrentGuildBuffProjection(session, guild, false, "guild_management"); err != nil {
return err
}
return s.writeCurrentGuildRanking(session, guild)
}
func (s *Service) publishCurrentGuildManagement(session *gameSession, guild *dnfguild.Guild) error {
for id := range guild.Members {
if id == strconv.Itoa(int(session.selectedCharacterID)) {
continue
}
if target, found := s.onlineGameSessionCharacterSnapshot(uint16(currentGuildNumericID(id))); found {
if err := s.sendCurrentGuildManagementStateToPeer(target.session, guild); err != nil {
s.logGameEvent(target.session, "game-guild-management-refresh-failed", "error", err)
}
}
}
return s.sendCurrentGuildManagementState(session, guild)
}
func (s *Service) handleCurrentGuildMark(session *gameSession, body []byte) error {
opcode := uint16(dnfenum.CmdPacketChangeGuildMark)
if len(body) != 1 || body[0] > 4 {
return s.sendGameUpperFailure(session, opcode, currentGuildGenericError)
}
ctx, cancel := s.currentGuildContext()
defer cancel()
err := s.mutateCurrentGuild(ctx, func(repositories dnfrepo.Group, registry *dnfguild.Registry) error {
character, err := selectedGuildCharacter(ctx, repositories, session)
if err != nil || character.AccountID != s.accountIDForSession(session) {
return dnfguild.ErrCharacterNotFound
}
return registry.SetMark(character.CharacterID, body[0], s.gameplayNow())
})
return s.finishCurrentGuildSettings(session, opcode, err, nil)
}
func (s *Service) handleCurrentGuildRecommendedChannel(session *gameSession, body []byte) error {
opcode := uint16(dnfenum.CmdPacketSetGuildRecommandChannel)
catalog := s.currentCatalog()
if len(body) != 0 || catalog == nil {
return s.sendGameUpperFailure(session, opcode, currentGuildChannelSettingError)
}
// The request carries no channel: the client means "the channel I am
// standing in now". The guard used to require ResidentFor(channel).ID ==
// channel.ID, which is false for every ordinary town channel because
// isResidentChannel treats type 11 as a death-tower channel (see
// channelcatalog.SettableGuildChannel), so every 保存 was answered with
// error 19 and the client showed 只有公会长才能设置.
channel, found := catalog.Channel(session.channel.ID)
advertise := s.channelAdvertiseID()
// The bounds mirror Registry.SetRecommendedChannel (region <= 249, channel
// 1..255) so a rejected channel keeps op830's own error instead of falling
// through to the generic one.
if !found || !catalog.SettableGuildChannel(channel) ||
advertise < 0 || advertise > 249 || channel.ID <= 0 || channel.ID > 255 {
return s.sendGameUpperFailure(session, opcode, currentGuildChannelSettingError)
}
ctx, cancel := s.currentGuildContext()
defer cancel()
err := s.mutateCurrentGuild(ctx, func(_ dnfrepo.Group, registry *dnfguild.Registry) error {
return registry.SetRecommendedChannel(strconv.Itoa(int(session.selectedCharacterID)), uint32(advertise), uint32(channel.ID), s.gameplayNow())
})
if err != nil {
return s.finishCurrentGuildSettings(session, opcode, err, nil)
}
// The channel is stored as the region the client advertises for its channel
// lookups plus the catalog channel id, which is the number the guild window
// renders as ch<id>.<name>. Re-project the saved state to the actor as well,
// so the dialog shows the new guild channel without reopening it.
if err := s.sendGameUpperSuccess(session, opcode, nil); err != nil {
return err
}
guild, guildErr := s.currentGuildForSelectedCharacter(session)
if guildErr != nil || guild == nil {
return guildErr
}
if err := s.sendCurrentGuildManagementState(session, guild); err != nil {
return err
}
return s.publishCurrentGuildManagement(session, guild)
}
func (s *Service) finishCurrentGuildSettings(session *gameSession, opcode uint16, mutationErr error, response []byte) error {
if mutationErr != nil {
return s.sendGameUpperFailure(session, opcode, currentGuildGenericError)
}
if err := s.sendGameUpperSuccess(session, opcode, response); err != nil {
return err
}
guild, err := s.currentGuildForSelectedCharacter(session)
if err != nil || guild == nil {
return err
}
return s.publishCurrentGuildManagement(session, guild)
}
func parseCurrentGuildRecruitment(body []byte) (uint32, string, bool, uint32, byte, bool) {
if len(body) < 14 {
return 0, "", false, 0, 0, false
}
mask := binary.LittleEndian.Uint32(body)
promotion, next, ok := parseCurrentGuildDSTR(body, 4)
if !ok || next != len(body)-6 || body[next] > 1 {
return 0, "", false, 0, 0, false
}
return mask, promotion, body[next] == 1, binary.LittleEndian.Uint32(body[next+1:]), body[next+5], true
}
// grantCurrentGuildAttendance applies the daily guild attendance that grants one
// 公会积分 to the member and appends the MileageHistory row 公会积分 lists.
//
// The current client has no 签到 button: the game signs in automatically on the
// first login of the day, so the grant hangs off the online publish, where
// Registry.Attend's once-per-local-day idempotence makes repeated calls harmless.
//
// It must never wait for guildMu. The online publish can run inside a guild
// critical section, guildMu is a plain (non-reentrant) mutex, and the first
// version of this hook called mutateCurrentGuild directly: a login then
// deadlocked its own session goroutine, the client hung on the loading screen and
// no further packet was answered (live 2026-09-12 01:33 -- both sessions stopped
// sending keepalives while the accept loop stayed healthy). If the lock is busy
// the grant is simply skipped and the next online publish applies it.
//
// Nothing is pushed to the client here either: 公会积分 and 今日签到成员 are both
// request-driven (op747 / op746), so the new row shows up the next time the window
// is opened, without risking a stray write from a non-session goroutine.
func (s *Service) grantCurrentGuildAttendance(session *gameSession, source string) {
if s == nil || session == nil || session.selectedCharacterID == 0 {
return
}
characterID := strconv.Itoa(int(session.selectedCharacterID))
// Bounded retry in a background goroutine, never on the caller's: the session
// path must return immediately even if another login holds guildMu.
go s.applyCurrentGuildAttendance(characterID, source)
}
// currentGuildAttendanceGrantTimeout bounds the background grant's wait for
// guildMu; a busy lock is retried and then dropped, and the next online publish
// (any area change) applies the attendance instead.
const currentGuildAttendanceGrantTimeout = 10 * time.Second
func (s *Service) applyCurrentGuildAttendance(characterID, source string) {
if strings.TrimSpace(characterID) == "" {
return
}
deadline := time.Now().Add(currentGuildAttendanceGrantTimeout)
for {
if s.guildMu.TryLock() {
s.guildMu.Unlock()
break
}
if time.Now().After(deadline) {
s.logPacketEvent("game-guild-attendance-skipped",
"reason", "guild_lock_busy", "source", source, "char_id", characterID)
return
}
time.Sleep(100 * time.Millisecond)
}
ctx, cancel := s.currentGuildContext()
defer cancel()
granted := false
mileage := uint32(0)
if err := s.mutateCurrentGuild(ctx, func(_ dnfrepo.Group, registry *dnfguild.Registry) error {
guild, _, member := registry.GuildForCharacter(characterID)
if !member || guild == nil {
return nil // not a member: nothing to sign in for
}
applied, err := registry.Attend(characterID, s.gameplayNow())
if err != nil {
return err
}
granted, mileage = applied, guild.Mileage
return nil
}); err != nil {
s.logPacketEvent("game-guild-attendance-grant-failed", "source", source, "char_id", characterID, "error", err)
return
}
if !granted {
return
}
s.logPacketEvent("game-guild-attendance-granted",
"source", source, "char_id", characterID, "mileage", mileage)
}
// publishCurrentGuildPresence refreshes the guild window of every other online
// member after characterID's online state changed. The current client has no
// per-member presence packet — the roster map at guild+0x2D8 is an internal
// index, not a wire target — so the refresh reuses the packets a member-list
// request already produces: op70/op127 carry the online count and the viewer's
// own grade, and the member list is replayed only for sessions that already
// asked for it this session, so an unsolicited op67 can never raise the guild
// panel on a client that never opened it.
func (s *Service) publishCurrentGuildPresence(characterID uint16, source string) {
if s == nil || characterID == 0 {
return
}
ctx, cancel := s.currentGuildContext()
defer cancel()
var guild *dnfguild.Guild
if err := s.readCurrentGuild(ctx, func(_ dnfrepo.Group, registry dnfguild.Registry) error {
if found, _, ok := registry.GuildForCharacter(strconv.Itoa(int(characterID))); ok {
guild = found
}
return nil
}); err != nil {
s.logPacketEvent("game-guild-presence-load-failed", "char_id", characterID, "source", source, "error", err)
return
}
if guild == nil {
return
}
for id := range guild.Members {
memberID := currentGuildNumericID(id)
if memberID == 0 || uint16(memberID) == characterID {
continue
}
target, online := s.onlineGameSessionCharacterSnapshot(uint16(memberID))
if !online || target.session == nil {
continue
}
if err := s.sendCurrentGuildManagementStateToPeer(target.session, guild); err != nil {
s.logGameEvent(target.session, "game-guild-presence-refresh-failed",
"char_id", characterID, "source", source, "error", err)
continue
}
if !currentGuildMemberListRequested(target.session) {
continue
}
if err := s.sendGameUpperSuccess(
target.session,
uint16(dnfenum.CmdPacketGuildMemerList),
buildCurrentGuildMemberListBody(guild, true, false,
s.currentGuildOnlineChannels(guild, target.session), s.gameplayNow()),
); err != nil {
s.logGameEvent(target.session, "game-guild-presence-member-list-failed",
"char_id", characterID, "source", source, "error", err)
}
}
}
// currentGuildNoticeMessageOpcode is GUILD_MESSAGE_TO_GUILD_MEMBER: opcode 141
// carried as class 0. It is a different packet from the class-1 op141
// member-list continuation (dnfenum.CmdPacketGuildAllMemberListNext) -- the
// client keeps one handler table per classification, registering sub_1D21FD0
// for op141 in the class-1 table (01D36A7D) and sub_1D85370 in the class-0 one
// (01D92AD2). sub_1D85370 reads a single 0x100-unit DSTR (01D853D0) and shows
// it, and it is the only packet that displays a changed notice without a
// relog: op70's own display is gated on the one-shot global flag at 0x519297C
// (01D8B8A2 / 01D8B8BD), so a later op70 updates guild+0x18 -- the field the
// edit box refills from -- without redisplaying anything. 86JP sends the same
// packet from GuildPacketBuilder.BuildGuildNoticeMessage.
//
// (An earlier version of this comment called 0x1A41 a display template. It is
// not: sub_367BE00(0x1A41) returns the format string sub_3457D20 uses to read
// the DSTR, the same role 0x334/0x335 play in the member-row parse. The string
// op70 formats the visible line with is 0x1A46.)
const currentGuildNoticeMessageOpcode = uint16(141)
// currentGuildMemberRegion is the region/server byte a member row carries at
// member+0x182 and an op1454 settings row carries at row+4. The two must be
// equal, because sub_FBE4D0 joins a settings row to a member record on
// (name, region).
//
// It cannot be 0. sub_325F590 stores this byte on the row control at +0x4C
// (0325F661 loads the row+0x182 argument, 0325F669 writes it) and the member
// context menu hands that field to sub_26A3590 as the op126 request's first
// byte. sub_26A3590 opens with `lea eax,[esi-1]; cmp eax,0xF8; ja bail`, so 0
// underflows to 0xFFFFFFFF and the request is abandoned inside the client --
// which is why clicking 任命副会长 produced no packet at all, confirmed against
// the game-guild-request log.
//
// 1 is what channelServerID() reports here (its default, no config override)
// and the value the client itself tests for at 00FC1813 / 00F9E3BD. What
// leaving 0 bought, and this gives up: sub_2699B10 treats a 0 request as
// "match any region" (02699BAB test ecx,ecx / je), so with a real region the
// live-actor lookup must now match actor+0x38C (sub_26908C0). If it does not,
// member+0x3C stops resolving and rows render from the fields this server
// sends instead of from the live actor -- they stay listed either way, since
// rebuild pass 2 keeps an online row whose +0x3C is 0. Reverting is this one
// constant.
const currentGuildMemberRegion = byte(1)
func buildCurrentGuildNoticeMessageBody(notice string) []byte {
var writer packetWriter
writeClientNameDstr(&writer, notice)
return writer.bytes()
}
// publishCurrentGuildNotice pushes the notice to every online member. The
// town-actor gate is the same one sendCurrentGuildManagementState uses: a
// class-0 packet that lands mid-scene-construction is what crashed character
// selection on 2026-09-08, and a member still loading rereads the notice from
// op70 anyway.
func (s *Service) publishCurrentGuildNotice(guild *dnfguild.Guild) {
if s == nil || guild == nil {
return
}
body := buildCurrentGuildNoticeMessageBody(guild.Notice)
for id := range guild.Members {
memberID := currentGuildNumericID(id)
if memberID == 0 {
continue
}
target, online := s.onlineGameSessionCharacterSnapshot(uint16(memberID))
if !online || target.session == nil || !currentGuildTownActorCommitted(target.session) {
continue
}
if err := s.sendGameUpperRawClass(target.session, currentGuildNoticeMessageOpcode, body, 0); err != nil {
s.logGameEvent(target.session, "game-guild-notice-publish-failed", "error", err)
}
}
}
// republishCurrentGuildMemberLists replays op67 for every online member that
// already opened the guild panel this session. A member row carries the grade
// at member+0x144 (sub_1D21FD0 read 12, cross-checked against sort column 5 at
// 00FA72AB), so a grade change stays invisible until the list is sent again --
// op70/op127 only carry the header counters and the viewer's own grade. The
// same memberListRequested gate publishCurrentGuildPresence uses keeps an
// unsolicited op67 from raising the panel on a client that never opened it.
func (s *Service) republishCurrentGuildMemberLists(guild *dnfguild.Guild) {
if s == nil || guild == nil {
return
}
now := s.gameplayNow()
for id := range guild.Members {
memberID := currentGuildNumericID(id)
if memberID == 0 {
continue
}
target, online := s.onlineGameSessionCharacterSnapshot(uint16(memberID))
if !online || target.session == nil || !currentGuildMemberListRequested(target.session) {
continue
}
if err := s.sendGameUpperSuccess(
target.session,
uint16(dnfenum.CmdPacketGuildMemerList),
buildCurrentGuildMemberListBody(guild, true, false,
s.currentGuildOnlineChannels(guild, target.session), now),
); err != nil {
s.logGameEvent(target.session, "game-guild-member-list-republish-failed", "error", err)
}
}
}
// finishCurrentGuildGradeChange is finishCurrentGuildSettings plus the member
// list replay a grade change needs.
func (s *Service) finishCurrentGuildGradeChange(session *gameSession, opcode uint16, mutationErr error, response []byte) error {
if err := s.finishCurrentGuildSettings(session, opcode, mutationErr, response); err != nil {
return err
}
if mutationErr != nil {
return nil
}
guild, err := s.currentGuildForSelectedCharacter(session)
if err != nil || guild == nil {
return err
}
s.republishCurrentGuildMemberLists(guild)
return nil
}
func markCurrentGuildMemberListRequested(session *gameSession) {
if session == nil {
return
}
session.guild.mu.Lock()
session.guild.memberListRequested = true
session.guild.mu.Unlock()
}
func currentGuildMemberListRequested(session *gameSession) bool {
if session == nil {
return false
}
session.guild.mu.Lock()
defer session.guild.mu.Unlock()
return session.guild.memberListRequested
}