diff --git a/_Refactor_/02_Group_Organizer/01_Goals.md b/_Refactor_/02_Group_Organizer/01_Goals.md new file mode 100644 index 0000000..c640a6b --- /dev/null +++ b/_Refactor_/02_Group_Organizer/01_Goals.md @@ -0,0 +1,15 @@ +# Refactor Goals: 02_Group_Organizer + +## 1. Primary Goal: Decouple UI from Core Logic + +The main objective is to refactor the Group Organizer to operate as a **Feature Module**. This means its internal components, specifically the UI and the core state management, must be fully decoupled from each other. + +- **From:** The UI (`ui/organizer/rosterBoard.lua`) directly calls functions in the core state module (`core/organizer/state.lua`) to pull data. This creates a tight, two-way dependency. +- **To:** The UI will become a passive listener. The core logic will become an independent announcer of state. They will communicate exclusively through `AceEvent-3.0`, establishing a clean, one-way data flow. + +## 2. Desired Outcome + +- **Core Logic as the "Single Source of Truth":** The `core/organizer/state.lua` module will be solely responsible for managing the roster data. It will not have any knowledge of the UI or any other module that might consume its data. +- **UI as a "Dumb" Renderer:** The `ui/organizer/rosterBoard.lua` module will be responsible only for rendering the data it is given. It will not contain any logic for fetching or managing state. It will simply listen for state change announcements and redraw itself accordingly. +- **Improved Modularity:** By breaking the direct link, both the core and UI components become more independent and easier to maintain or replace in the future. +- **Adherence to Architecture:** This refactor will align the Group Organizer with the established architectural pattern for "Feature Modules" in the addon. diff --git a/_Refactor_/02_Group_Organizer/02_Implementation_Plan.md b/_Refactor_/02_Group_Organizer/02_Implementation_Plan.md new file mode 100644 index 0000000..6d18958 --- /dev/null +++ b/_Refactor_/02_Group_Organizer/02_Implementation_Plan.md @@ -0,0 +1,74 @@ +# Refactor Implementation Plan: 02_Group_Organizer + +## 1. Core Logic (`core/organizer/state.lua`) Modifications + +The `state.lua` module will be modified to become the sole announcer of the roster's state. + +1. **Identify State-Changing Functions:** A thorough review of the file will be conducted to identify all public and private functions that result in a modification of the main roster table. This includes functions that add, remove, or update players. + +2. **Create a Centralized Broadcast Function:** A new private function, `BroadcastRosterUpdate()`, will be created within the module. + ```lua + local function BroadcastRosterUpdate() + -- The roster table is assumed to be a local variable 'roster' + NextKey:SendMessage("ORGANIZER_ROSTER_UPDATED", roster) + end + ``` + +3. **Integrate the Broadcast:** At the end of every identified state-changing function, a call to `BroadcastRosterUpdate()` will be added. This ensures that every change, no matter how small, triggers a state announcement. + +4. **Remove UI Dependencies:** Any code that suggests a dependency on the UI (e.g., checking if the UI is visible before processing data) will be removed. The core logic should be completely independent of any consumer. + +## 2. UI (`ui/organizer/rosterBoard.lua`) Modifications + +The `rosterBoard.lua` module will be refactored into a passive listener and renderer. + +1. **Remove Direct Calls:** All direct function calls to the `core/organizer/state.lua` module (or any of its equivalents for fetching data) will be located and removed. + +2. **Register Event Listener:** In the module's initialization function (e.g., `OnInitialize`), a listener for the new `ORGANIZER_ROSTER_UPDATED` event will be registered. + ```lua + function RosterBoard:OnInitialize() + -- ... other initialization + NextKey:RegisterMessage("ORGANIZER_ROSTER_UPDATED", self.OnRosterUpdated, self) + end + ``` + +3. **Implement the Event Handler:** A new method, `OnRosterUpdated`, will be created. This method will accept the roster data from the event payload. + ```lua + function RosterBoard:OnRosterUpdated(eventName, roster) + -- Store the new roster data locally + self.rosterData = roster + -- Trigger a full redraw of the UI + self:Redraw() + end + ``` + +4. **Adapt the Redraw Logic:** The existing `Redraw()` function (or its equivalent) will be modified to source its data exclusively from the locally stored `self.rosterData`. It will no longer fetch data on its own. It will be responsible for iterating over the data and rendering the player cards, sorting headers, and any other relevant UI elements. + +## 3. Data Flow Diagram + +The resulting data flow will be a simple, one-way push from the core to the UI. + +``` ++----------------------------+ +| core/organizer/state.lua | ++----------------------------+ +| | +| - Roster data is changed | +| - Calls BroadcastRoster() | +| | ++-------------+--------------+ + | + | Fires Event: "ORGANIZER_ROSTER_UPDATED" + | Payload: (roster_table) + v ++-------------+--------------+ +| ui/organizer/rosterBoard.lua| ++----------------------------+ +| | +| - Listens for event | +| - Calls OnRosterUpdated() | +| - Stores new roster data | +| - Calls self:Redraw() | +| | ++----------------------------+ +``` diff --git a/_Refactor_/02_Group_Organizer/03_Event_Map.md b/_Refactor_/02_Group_Organizer/03_Event_Map.md new file mode 100644 index 0000000..8e78005 --- /dev/null +++ b/_Refactor_/02_Group_Organizer/03_Event_Map.md @@ -0,0 +1,32 @@ +# Refactor Event Map: 02_Group_Organizer + +## 1. Module Type: Feature (Stateful, Decoupled Component) + +The Group Organizer is classified as a **Feature Module**. Its purpose is to provide a user-facing feature that has its own internal state, UI, and core logic. It is an active component that needs to communicate changes to other parts of the system without being tightly coupled to them. + +## 2. Communication Pattern: `AceEvent-3.0` (Publish/Subscribe) + +Based on its role as a feature, the primary method of communication between the Group Organizer's core logic and its UI (and any other potential features) will be the **`AceEvent-3.0` Publish/Subscribe system**. + +- **Publisher:** The core logic module (`core/organizer/state.lua`) will act as the publisher. It will announce changes to its state. +- **Subscriber:** The UI module (`ui/organizer/rosterBoard.lua`) will act as a subscriber. It will listen for state change announcements and react accordingly. + +This enforces the one-way data flow that is central to the addon's refactored architecture. + +## 3. Events Published by this Module + +--- + +### `ORGANIZER_ROSTER_UPDATED` + +- **Fired By:** `core/organizer/state.lua` +- **When:** Fired any time the group roster's data is modified (e.g., players added/removed, data updated, list re-sorted). +- **Payload:** + - `arg1` (table): The complete, authoritative table representing the new state of the group roster. +- **Purpose:** To inform any interested module that the roster has changed and provide the new data, enabling UI updates or other reactions without requiring the consumer to fetch the data itself. + +--- + +## 4. Events Listened For by this Module + +The core logic of the Group Organizer may listen for events from other systems in the future, but for the scope of this specific UI/core decoupling refactor, it does not need to listen for any new events. The UI will listen for the event defined above. diff --git a/core/organizer/state.lua b/core/organizer/state.lua index c6340bb..3b36dfb 100644 --- a/core/organizer/state.lua +++ b/core/organizer/state.lua @@ -17,6 +17,20 @@ OrganizerState.groups = {} -- {[groupIndex][slotIndex] = playerID} - Group OrganizerState.keystones = {} -- {[groupIndex] = {keystone, playerID}} - Designated keystones OrganizerState.activePoll = nil -- {id, startTime, responses, timeout} - Active poll state +-- MARK: Private Methods +--- Broadcasts the current roster state to all listeners. +local function BroadcastRosterUpdate() + Debug:Dev("organizer_state", "Broadcasting ORGANIZER_ROSTER_UPDATED") + local rosterData = { + players = OrganizerState.players, + bench = OrganizerState.bench, + optOut = OrganizerState.optOut, + groups = OrganizerState.groups, + keystones = OrganizerState.keystones, + } + NextKey222:SendMessage("ORGANIZER_ROSTER_UPDATED", rosterData) +end + -- MARK: Initialization function OrganizerState:Initialize() return NextKey222.SafeRun(function() @@ -109,6 +123,7 @@ function OrganizerState:SetPlayer(playerID, playerData) Debug:Dev("organizer_state", "SetPlayer:", playerID, "- stored with roles:", playerData.roles and table.concat(playerData.roles, ",") or "NONE", "specPreferences:", playerData.specPreferences ~= nil, "specDetails:", playerData.specDetails ~= nil) + BroadcastRosterUpdate() end, "OrganizerState:SetPlayer") end @@ -135,6 +150,7 @@ function OrganizerState:UpdatePlayer(playerID, updates) self.players[playerID] = playerData Debug:Dev("organizer_state", "UpdatePlayer:", playerID, "- updates applied") + BroadcastRosterUpdate() end, "OrganizerState:UpdatePlayer") end @@ -181,7 +197,7 @@ function OrganizerState:UpdatePlayerFromPollResponse(playerID, response) Debug:Dev("organizer_state", "UpdatePlayerFromPollResponse:", playerID, "- specPreferences:", playerData.specPreferences ~= nil, "specDetails:", playerData.specDetails ~= nil) - + BroadcastRosterUpdate() end, "OrganizerState:UpdatePlayerFromPollResponse") end @@ -214,6 +230,9 @@ function OrganizerState:RemovePlayer(playerID) end Debug:Dev("organizer_state", "RemovePlayer:", playerID, "- removed:", existed) + if existed then + BroadcastRosterUpdate() + end return existed end, "OrganizerState:RemovePlayer") @@ -311,6 +330,7 @@ function OrganizerState:MoveToBench(playerID) self.bench[playerID] = true Debug:Dev("organizer_state", "MoveToBench:", playerID) + BroadcastRosterUpdate() return true end, "OrganizerState:MoveToBench") end @@ -340,6 +360,7 @@ function OrganizerState:MoveToOptOut(playerID) self.optOut[playerID] = true Debug:Dev("organizer_state", "MoveToOptOut:", playerID) + BroadcastRosterUpdate() return true end, "OrganizerState:MoveToOptOut") end @@ -398,6 +419,7 @@ function OrganizerState:MoveToSlot(playerID, groupIndex, slotIndex) end Debug:User("AFTER SAVE - Total slots in memory:", totalSlots) + BroadcastRosterUpdate() return true end, "OrganizerState:MoveToSlot") end @@ -478,6 +500,7 @@ function OrganizerState:AssignToGroup(playerID, groupIndex, slotIndex) self.groups[groupIndex][slotIndex] = playerID Debug:Dev("organizer_state", "AssignToGroup:", playerID, "to group", groupIndex, "slot", slotIndex) + BroadcastRosterUpdate() return true end, "OrganizerState:AssignToGroup") end @@ -505,6 +528,9 @@ function OrganizerState:UnassignFromGroup(playerID) end Debug:Dev("organizer_state", "UnassignFromGroup:", playerID, "- was in group:", wasInGroup) + if wasInGroup then + BroadcastRosterUpdate() + end return wasInGroup end, "OrganizerState:UnassignFromGroup") end @@ -587,6 +613,7 @@ function OrganizerState:DesignateKeystone(groupIndex, playerID, keystone) } Debug:Dev("organizer_state", "DesignateKeystone: group", groupIndex, "keystone from", playerID) + BroadcastRosterUpdate() return true end, "OrganizerState:DesignateKeystone") end @@ -605,6 +632,9 @@ function OrganizerState:ClearKeystone(groupIndex) self.keystones[groupIndex] = nil Debug:Dev("organizer_state", "ClearKeystone: group", groupIndex, "- had keystone:", hadKeystone) + if hadKeystone then + BroadcastRosterUpdate() + end return hadKeystone end, "OrganizerState:ClearKeystone") end @@ -938,6 +968,7 @@ function OrganizerState:LoadFromPersistence() Debug:Dev("organizer_state", "LoadFromPersistence: Restored", restoredCount, "players from SavedVariables") + BroadcastRosterUpdate() return true end, "OrganizerState:LoadFromPersistence") @@ -970,6 +1001,7 @@ function OrganizerState:ClearPersistedData() Debug:User("Poll data cleared successfully") Debug:Dev("organizer_state", "ClearPersistedData: All state cleared") + BroadcastRosterUpdate() return true end, "OrganizerState:ClearPersistedData") @@ -1020,4 +1052,4 @@ function OrganizerState:PrintState() print("=========================") end, "OrganizerState:PrintState") -end \ No newline at end of file +end diff --git a/ui/organizer/rosterBoard.lua b/ui/organizer/rosterBoard.lua index df41a2e..88e1211 100644 --- a/ui/organizer/rosterBoard.lua +++ b/ui/organizer/rosterBoard.lua @@ -16,6 +16,15 @@ RosterBoard.optOutSection = nil RosterBoard.viewMode = nil -- "ORGANIZER" or "PARTICIPANT" RosterBoard.manualGroupCount = nil -- nil = auto, number = manual override +-- NEW: Local copy of the roster state, updated only via event +RosterBoard.rosterState = { + players = {}, + bench = {}, + optOut = {}, + groups = {}, + keystones = {} +} + -- Poll state RosterBoard.activePoll = nil @@ -51,27 +60,41 @@ function RosterBoard:Initialize() -- Determine view mode self.viewMode = self:DetermineViewMode() - -- Initialize arrays + -- Initialize arrays and state self.benchCards = {} self.groupSlots = {} self.groupBackgrounds = {} self.groupTitles = {} self.groupKeystones = {} self.allInteractiveFrames = {} + self.rosterState = { players = {}, bench = {}, optOut = {}, groups = {}, keystones = {} } - -- Note: Spec change events are handled by ProfilesService which automatically - -- triggers UI refresh. No need for duplicate event handlers here. - Debug:Dev("organizer_ui", "Roster Board initialized (spec changes handled by ProfilesService)") + -- REFACTOR: Register for state update events + NextKey222:RegisterMessage("ORGANIZER_ROSTER_UPDATED", self.OnRosterUpdated, self) - Debug:Dev("organizer_ui", "Roster Board initialized successfully") + Debug:Dev("organizer_ui", "Roster Board initialized and subscribed to ORGANIZER_ROSTER_UPDATED") return true end, "RosterBoard:Initialize") end --- MARK: Event Registration & Card Refresh --- Note: Spec change events are handled by ProfilesService (core/profiles.lua:132-302) --- ProfilesService automatically invalidates cache and triggers UI refresh when specs change --- This prevents duplicate event handlers and ensures consistent behavior across all UI components +-- MARK: Event Handlers +--- Handles the ORGANIZER_ROSTER_UPDATED event from the core state module. +-- @param eventName string - The name of the event ("ORGANIZER_ROSTER_UPDATED") +-- @param rosterData table - The complete, new state of the roster. +function RosterBoard:OnRosterUpdated(eventName, rosterData) + return NextKey222.SafeRun(function() + Debug:Dev("organizer_ui", "Received ORGANIZER_ROSTER_UPDATED event") + + -- Store the new authoritative state + self.rosterState = rosterData + + -- If the UI is visible, trigger a full redraw to reflect the new state + if self:IsVisible() then + Debug:Dev("organizer_ui", "UI is visible, triggering full redraw") + self:PopulateAllSections() + end + end, "RosterBoard:OnRosterUpdated") +end -- MARK: View Mode Detection function RosterBoard:DetermineViewMode() @@ -174,7 +197,6 @@ function RosterBoard:CreateMainFrame() -- Apply view-specific restrictions if self:IsParticipant() then self:DisableOrganizerControls() - self:RequestRosterState() end Debug:Dev("organizer_ui", "Created Roster Board main frame in", self.viewMode, "mode") @@ -361,6 +383,7 @@ end -- MARK: Layout Calculation (Uses UIConfig constants) function RosterBoard:CalculateOptimalLayout() + -- REFACTOR: Use the local rosterState for layout calculations local benchPlayers = self:GetBenchPlayers() or {} local groupedPlayers = self:GetGroupedPlayers() or {} local playerCount = #benchPlayers + #groupedPlayers @@ -415,92 +438,86 @@ function RosterBoard:CalculateOptimalLayout() } end +-- REFACTOR: These functions now pull from the local `rosterState` function RosterBoard:GetBenchPlayers() - return NextKey222.BenchManager:get_bench_players(self) + local benchPlayerIDs = {} + for playerID, isBenched in pairs(self.rosterState.bench or {}) do + if isBenched then + table.insert(benchPlayerIDs, playerID) + end + end + + local benchPlayersData = {} + for _, playerID in ipairs(benchPlayerIDs) do + if self.rosterState.players and self.rosterState.players[playerID] then + table.insert(benchPlayersData, self.rosterState.players[playerID]) + end + end + return benchPlayersData end function RosterBoard:GetGroupedPlayers() - return NextKey222.SlotManager:get_grouped_players(self) + local groupedPlayersData = {} + if not self.rosterState.groups then return groupedPlayersData end + + for _, slots in pairs(self.rosterState.groups) do + for _, playerID in pairs(slots) do + if self.rosterState.players and self.rosterState.players[playerID] then + table.insert(groupedPlayersData, self.rosterState.players[playerID]) + end + end + end + return groupedPlayersData end -- MARK: Data Population function RosterBoard:PopulateAllSections() return NextKey222.SafeRun(function() - Debug:Dev("organizer_ui", "PopulateAllSections called") - - local allPlayers = self:GetBenchPlayers() - Debug:Dev("organizer_ui", "Got", allPlayers and #allPlayers or 0, "players") - - local benchPlayers = allPlayers or {} - + Debug:Dev("organizer_ui", "PopulateAllSections called - drawing from local rosterState") + + -- REFACTOR: Use local roster data + local benchPlayers = self:GetBenchPlayers() if #benchPlayers > 0 then Debug:Dev("organizer_ui", "Populating bench with", #benchPlayers, "players") self:PopulateBench(benchPlayers) end - - -- CRITICAL FIX: Restore group slot assignments from OrganizerState - Debug:Dev("organizer_ui", "Restoring group slot assignments from state") - - -- DEBUG: Check if OrganizerState has any group data - local totalGroupSlots = 0 - if NextKey222.OrganizerState and NextKey222.OrganizerState.groups then - for gIdx, slots in pairs(NextKey222.OrganizerState.groups) do - for sIdx, pID in pairs(slots) do - totalGroupSlots = totalGroupSlots + 1 - Debug:Dev("organizer_ui", "State has player in group", gIdx, "slot", sIdx, ":", pID) - end - end - end - Debug:Dev("organizer_ui", "OrganizerState has", totalGroupSlots, "total players in group slots") - - if self.groupSlots then + + -- REFACTOR: Restore group slot assignments from local rosterState + if self.groupSlots and self.rosterState.groups then local restoredCount = 0 for groupIndex, slots in pairs(self.groupSlots) do for slotIndex, slot in pairs(slots) do - -- Get playerID from state (SafeRun returns the value directly, not (success, result)) - local playerID = NextKey222.OrganizerState:GetSlotPlayer(groupIndex, slotIndex) + local playerID = self.rosterState.groups[groupIndex] and self.rosterState.groups[groupIndex][slotIndex] - -- CRITICAL: Check playerID is valid before using it - -- SafeRun may return true/false for success, or the actual value - -- We only want string playerIDs if type(playerID) == "string" and playerID ~= "" then - Debug:Dev("organizer_ui", "Found player in group", groupIndex, "slot", slotIndex, "- playerID:", playerID) - -- Fetch full player data object using the playerID - local playerData = NextKey222.OrganizerState:GetPlayer(playerID) + local playerData = self.rosterState.players and self.rosterState.players[playerID] if playerData and type(playerData) == "table" then - -- Create card for this slot using the FULL player data object - local card = NextKey222.PlayerCard:CreateNativeCard( - playerData, - slot, - "role_slot", - "compact" -- Start compact, will expand on placement - ) + local card = NextKey222.PlayerCard:CreateNativeCard(playerData, slot, "role_slot", "compact") if card then - -- Place card in slot using SlotManager NextKey222.SlotManager:place_card_in_slot(card, slot) restoredCount = restoredCount + 1 - Debug:Dev("organizer_ui", "Restored player to group", groupIndex, "slot", slotIndex, ":", playerID) - else - Debug:Error("Failed to create card for slot player:", playerID) end - else - Debug:Error("GetPlayer returned invalid data for playerID:", playerID, "- type:", type(playerData)) end end end end Debug:Dev("organizer_ui", "Restored", restoredCount, "players to group slots") end + + -- REFACTOR: Populate opt-out from local rosterState + local optOutPlayerIDs = {} + for playerID, isOptOut in pairs(self.rosterState.optOut or {}) do + if isOptOut then + table.insert(optOutPlayerIDs, playerID) + end + end - -- SESSION 4 FIX: Fetch and populate opt-out players from state - local optOutPlayerIDs = NextKey222.OrganizerState:GetOptOutPlayers() local optOutPlayers = {} for _, playerID in ipairs(optOutPlayerIDs) do - local playerData = NextKey222.OrganizerState:GetPlayer(playerID) - if playerData then - table.insert(optOutPlayers, playerData) + if self.rosterState.players and self.rosterState.players[playerID] then + table.insert(optOutPlayers, self.rosterState.players[playerID]) end end @@ -1157,44 +1174,34 @@ end -- @param groupIndex number - Group number -- @return string|nil - Formatted group announcement or nil if empty function RosterBoard:FormatSingleGroupAnnouncement(groupIndex) - if not NextKey222.OrganizerState then - Debug:Error("OrganizerState not available") + if not self.rosterState then + Debug:Error("rosterState not available") return nil end - - -- Get group assignments (unwrap SafeRun tuple) - local success, assignments = NextKey222.OrganizerState:GetGroupAssignments(groupIndex) - if not success or not assignments or not next(assignments) then - -- Check if there are any slots at all + + local assignments = self.rosterState.groups and self.rosterState.groups[groupIndex] + if not assignments or not next(assignments) then if not self.groupSlots or not self.groupSlots[groupIndex] then - return nil -- Skip completely empty groups + return nil end assignments = {} end + + local keystoneData = self.rosterState.keystones and self.rosterState.keystones[groupIndex] - -- Get keystone data (unwrap SafeRun tuple) - local success2, keystoneData = NextKey222.OrganizerState:GetDesignatedKeystone(groupIndex) - if not success2 then - keystoneData = nil - end - - -- Build announcement lines local lines = {} - - -- Add group header local header = self:FormatGroupHeader(groupIndex, keystoneData) if header then table.insert(lines, header) end - -- Add player lines local playerCount = 0 for slotIndex = 1, 5 do local playerID = assignments and assignments[slotIndex] if playerID then - local pSuccess, playerData = NextKey222.OrganizerState:GetPlayer(playerID) - if pSuccess and playerData and type(playerData) == "table" then + local playerData = self.rosterState.players and self.rosterState.players[playerID] + if playerData and type(playerData) == "table" then local playerLine = self:FormatPlayerLine(playerData, slotIndex) if playerLine then table.insert(lines, playerLine) @@ -1204,7 +1211,6 @@ function RosterBoard:FormatSingleGroupAnnouncement(groupIndex) end end - -- Add PUG needs if there are any players if playerCount > 0 then local pugNeeds = self:IdentifyPUGNeeds(groupIndex) if pugNeeds then @@ -1213,7 +1219,6 @@ function RosterBoard:FormatSingleGroupAnnouncement(groupIndex) end end else - -- Empty group table.insert(lines, " No players assigned") end @@ -1737,35 +1742,7 @@ function RosterBoard:FindCompatibleSlotInGroup(card, groupIndex) return NextKey222.CardMovement:find_compatible_slot_in_group(self, card, groupIndex) end --- MARK: State Synchronization -function RosterBoard:BroadcastRosterUpdate(updateData) - if NextKey222.Communications and NextKey222.Communications.QueueOrganizerUpdate then - NextKey222.Communications:QueueOrganizerUpdate(updateData) - end -end - -function RosterBoard:RequestRosterState() - Debug:Dev("org_sync", "Requesting full roster state from organizer") -end - -function RosterBoard:OnRosterUpdateReceived(message, sender) - return NextKey222.SafeRun(function() - if self:IsOrganizer() then - return - end - - local updateData = message.data - - if updateData.action == "CARD_MOVED" then - Debug:Dev("org_sync", "Received CARD_MOVED update") - elseif updateData.action == "KEYSTONE_DESIGNATED" then - Debug:Dev("org_sync", "Received KEYSTONE_DESIGNATED update") - elseif updateData.action == "ROSTER_STATE_FULL" then - Debug:Dev("org_sync", "Received ROSTER_STATE_FULL update") - end - - end, "RosterBoard:OnRosterUpdateReceived") -end +-- MARK: State Synchronization (OBSOLETE with AceEvent refactor) -- MARK: Public Interface function RosterBoard:Show() @@ -2146,8 +2123,13 @@ function RosterBoard:SyncUIToState() end -- Get current state - local benchPlayerIDs = NextKey222.OrganizerState:GetBenchPlayers() - local optOutPlayerIDs = NextKey222.OrganizerState:GetOptOutPlayers() + local benchPlayerIDs = self:GetBenchPlayers() + local optOutPlayerIDs = {} + for playerID, isOptOut in pairs(self.rosterState.optOut or {}) do + if isOptOut then + table.insert(optOutPlayerIDs, playerID) + end + end -- Clear existing bench cards for _, card in ipairs(self.benchCards) do @@ -2172,24 +2154,21 @@ function RosterBoard:SyncUIToState() self.optOutSection.playerCards = {} -- Rebuild bench from state - for _, playerID in ipairs(benchPlayerIDs) do - local playerData = NextKey222.OrganizerState:GetPlayer(playerID) - if playerData then - local card = NextKey222.PlayerCard:CreateNativeCard( - playerData, - self.benchContainer, - "bench", - "compact" - ) - if card then - table.insert(self.benchCards, card) - end + for _, playerData in ipairs(self:GetBenchPlayers()) do + local card = NextKey222.PlayerCard:CreateNativeCard( + playerData, + self.benchContainer, + "bench", + "compact" + ) + if card then + table.insert(self.benchCards, card) end end -- Rebuild opt-out from state for _, playerID in ipairs(optOutPlayerIDs) do - local playerData = NextKey222.OrganizerState:GetPlayer(playerID) + local playerData = self.rosterState.players and self.rosterState.players[playerID] if playerData then local card = NextKey222.PlayerCard:CreateNativeCard( playerData,