• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Lua] Dialog Queue

Status
Not open for further replies.
Level 6
Joined
Jun 30, 2017
Messages
41
Dialog Queue and some dialog extensions.
A Dialog Queueing System to queue up dialogs, rather than forcibly closing one when another one needs to get displayed. MultiOptionDialog library to support cycling dialog buttons, and Voting Dialog. More details and information about these resources is found in their respective spoilers.

Need to do more tests on these before putting them on submissions, but in the meantime, suggestions and critiques are welcome!

Lua:
do
--[[
    DialogQueue v1.0 by Insanity_AI
    ------------------------------------------------------------------------------
    A system that fixes WC3's default dialog behavior by hijacking Dialog natives:
        - Calling DialogDisplay on a new dialog when another dialog is currently being displayed
          will no longer hide the currently displayed dialog to show the new dialog, but will wait
          for the old dialog to be clicked (or hidden) before showing the new dialog.
        - Calling DialogClear on dialogs being currently shown will not just wipe all its content
          and keep displaying a blank dialog to player, but will hide the dialog as well.
   
    Additionally:
        - Calling DialogDisplay or DialogDestroy on a dialog that is queued up will remove it from the queue, as it is blank.
        - Will always execute dialog events first, and then dialog button events.
        - Can be used without native override, and with callback functions instead of triggers.

    Requires:
        SetUtils - https://www.hiveworkshop.com/threads/set-group-datastructure.331886/
        Defintive Doubly Linked List - https://www.hiveworkshop.com/threads/definitive-doubly-linked-list.339392/
    Optional:
        Total Initialization - https://www.hiveworkshop.com/threads/total-initialization.317099/

    Installation:
        To enable dialog native override (for GUI), either call DialogQueue.NativeOverride on Map Initialization
            Or have TotalInitialization and set OVERRIDE_NATIVES to true.]]
    local OVERRIDE_NATIVES = true
    local OVERRIDE_GET_TRIGGERING_PLAYER = false --works only if OVERRIDE_NATIVES is also true.
--[[    Additionally, call DialogQueue.Init function on Game Start, if TotalInitialization is not present.

Hijacked Natives API:
    DialogCreate -> returns a DialogWrapper object
    DialogDestroy -> clears DialogWrapper's content and dequeues it from player queues.
    DialogClear -> same as DialogDestroy
    DialogSetMessage -> sets DialogWrapper's messageText property
    DialogAddButton -> adds a new DialogButtonWrapper object to DialogWrapper with text and hotkey.
    DialogAddQuitButton -> same as DialogAddButton but with quit functionality.
    DialogDisplay -> Enqueues or Dequeues the dialog to/from player's dialog queue (depending on flag argument)
    TriggerRegisterDialogEvent -> Adds the trigger to DialogWrapper object to be executed when dialog is clicked on.
    TriggerRegisterDialogButtonEvent -> Adds the trigger to DialogButtonWrapper object to be executed when dialog is clicked on.

    GetClickedButton -> returns DialogButtonWrapper object that was clicked.
    GetClickedDialog -> returns DialogWrapper object that was clicked.
    GetTriggerPlayer -> returns the player that did the clicking, if it was dialog related. Otherwise does what GetTriggerPlayer usually does.

DialogWrapper API:
    DialogWrapper.create(data?) -> creates a new dialog object to be used for this system.
                                -> data can be an existing DialogWrapper object to make another copy of.
    DialogWrapper.triggers      - array of triggers to be executed when dialog is clicked. (not nillable, must be at least empty table.)
    DialogWrapper.callback      - callback function for when dialog has been clicked.
    DialogWrapper.messageText   - Title text for dialog. (nillable)
    DialogWrapper.buttons       - array of DialogButtonWrapper objects (not nillable, must be at least empty table.)
    DialogWrapper.quitButton    - single DialogQuitButtonWrapper object (nillable)

DialogButtonWrapper API:
    DialogButtonWrapper.create(data)    -> creates a new DialogButtonWrapper
                                        -> data is an existing DialogButtonWrapper object to make another copy of.
    DialogButtonWrapper.text        - button's text (nillable)
    DialogButtonWrapper.hotkey      - button's hotkey (must be either integer or nillable)
    DialogButtonWrapper.triggers    - array of triggers to be executed when the button is clicked. (not nillable, must be at least empty table)
    DialogButtonWrapper.callback    - callback function for when dialog button has been clicked.

DialogQuitButtonWrapper API: (extension of DialogButtonWrapper)
    DialogQuitButtonWrapper.create(data)    -> creates a new DialogQuitButtonWrapper
                                            -> data is an existing DialogQuitButtonWrapper object to make another copy of.
    DialogQuitButtonWrapper.doScoreScreen - must be true or false (not nillable)

DialogQueue API:
    DialogQueue.Enqueue(dialogWrapper, player) -> queues up a dialog for the player
    DialogQueue.EnqueueAfterCurrentDialog(dialogWrapper, player) -> queues up a dialog right after the current dialog that is being displayed right now.
    DialogQueue.Dequeue(dialogWrapper, player) -> dequeues a dialog for the player
    DialogQueue.SkipCurrentDialog -> skips currently displayed dialog.
    DialogQueue.ClearEventResponsesForTrigger - clears trigger's dialog event responses from memory (avoids memory leaks in case of trigger destroy)
    DialogQueue.GetTriggerPlayer - gets the last player that clicked the dialog/button for specific trigger.
    DialogQueue.GetTriggerDialog - gets the last clicked dialog for that trigger.
    DialogQueue.GetTriggerButton - gets the last clicked button for that trigger.

Functionality:
    - Creates a single dialog for each player in the game, all registered to the same trigger that processes dialog clicks
        and passes them to respective triggers/callback functions. These WC3 dialogs are never removed but rather cleared and
        set with different buttons and messages.
    - Creates a single queue for each player designated to store DialogWrappers and show them sequentially in the queue order.
    - When a player presses a dialog button, the currently displayed dialog is removed from that player's queue and the system
        will show the next queued dialog, if there is any.
    - Queueing up a dialog when there are no dialogs in the queue will immediately display it to the player.
    - If the dialog has no buttons defined, the dialog will be skipped just before being displayed.
]]
-- concerns:
-- Event response getters not working (properly) if:
--      2 or more consecutive executions of same trigger (probably easily done with TSA or Polled Waits)
--      trigger gets destroyed on execution before all event response getter calls are called.


---@class DialogWrapper
---@field triggers trigger[]
---@field callback fun(dialog: DialogWrapper, player: player)
---@field messageText string
---@field buttons DialogButtonWrapper[]
---@field quitButton DialogQuitButtonWrapper
DialogWrapper = {}
DialogWrapper.__index = DialogWrapper

---@param data? DialogWrapper
---@return DialogWrapper
function DialogWrapper.create(data)
    local instance = setmetatable({}, DialogWrapper)

    instance.buttons = {}
    instance.triggers = {}
    if data ~= nil then
        for _, button in ipairs(data.buttons) do
            table.insert(instance.buttons, DialogButtonWrapper.create(button))
        end

        instance.triggers = {}
        for _, trigger in ipairs(data.triggers) do
            table.insert(instance.triggers, trigger)
        end

        instance.callback = data.callback
        instance.messageText = data.messageText

        if data.quitButton then
            instance.quitButton = DialogQuitButtonWrapper.create(data.quitButton)
        end
    end

    return instance
end

---@class DialogButtonWrapper
---@field text string
---@field hotkey integer|nil --1 character only
---@field triggers trigger[]
---@field callback fun(button: DialogButtonWrapper, dialog: DialogWrapper, player: player)
DialogButtonWrapper = {}
DialogButtonWrapper.__index = DialogButtonWrapper

---@param data? DialogButtonWrapper
---@return DialogButtonWrapper
function DialogButtonWrapper.create(data)
    local instance = setmetatable({}, DialogButtonWrapper)
    instance.triggers = {}
    if data ~= nil then
        instance.text = data.text
        instance.hotkey = data.hotkey
        instance.callback = data.callback
    end
    return instance
end

---@class DialogQuitButtonWrapper : DialogButtonWrapper
---@field doScoreScreen boolean
DialogQuitButtonWrapper = {}
DialogQuitButtonWrapper.__index = DialogQuitButtonWrapper
setmetatable(DialogQuitButtonWrapper, DialogButtonWrapper)

---@param data? DialogQuitButtonWrapper
---@return DialogQuitButtonWrapper
function DialogQuitButtonWrapper.create(data)
    local instance = setmetatable(DialogButtonWrapper(data), DialogQuitButtonWrapper)
    if data ~= nil then
        instance.doScoreScreen = data.doScoreScreen
    end
    return instance --[[@as DialogQuitButtonWrapper]]
end

local oldDialogCreate = DialogCreate
-- local oldDialogDestroy = DialogDestroy
local oldDialogClear = DialogClear
local oldDialogSetMessage = DialogSetMessage
local oldDialogAddButton = DialogAddButton
local oldDialogAddQuitButton = DialogAddQuitButton
local oldDialogDisplay = DialogDisplay

local oldTriggerRegisterDialogEvent = TriggerRegisterDialogEvent
-- local oldTriggerRegisterDialogButtonEvent = TriggerRegisterDialogButtonEvent

local oldGetClickedButton = GetClickedButton
-- local oldGetClickedDialog = GetClickedDialog

local oldGetTriggerPlayer = GetTriggerPlayer

DialogQueue = {}

local playerQueueActive = {} ---@type table<player, boolean>
local dialogQueues = {} ---@type table<player, LinkedListHead>
local playerDialog = {} ---@type table<player, dialog>
local currentDialogButtons = {} ---@type table<player, table<button, DialogButtonWrapper>>

---@param player player
local function displayNextDialog(player)

    if playerQueueActive[player] ~= nil then
        return -- is active
    end

    local playerQueue = dialogQueues[player]
    if playerQueue.n <= 0 then
        return -- empty nothing to show.
    end

    playerQueueActive[player] = true

    local dialog = playerDialog[player]
    local dialogDefinition = playerQueue.next.value --[[@as DialogWrapper]]

    oldDialogDisplay(player, dialog, false)
    oldDialogClear(dialog)
    if type(dialogDefinition.messageText) == "string" then
        oldDialogSetMessage(dialog, dialogDefinition.messageText)
    end

    currentDialogButtons[player] = {}
    if #dialogDefinition.buttons == 0 and dialogDefinition.quitButton == nil then
        -- dialog has no buttons, skip and show next dialog.
        DialogQueue.SkipCurrentDialog(player)
        playerQueueActive[player] = false
        displayNextDialog(player)
        return
    end

    for _, buttonDefinition in ipairs(dialogDefinition.buttons) do
        local button = oldDialogAddButton(dialog, buttonDefinition.text, buttonDefinition.hotkey)
        currentDialogButtons[player][button] = buttonDefinition
    end

    if dialogDefinition.quitButton ~= nil then
        local quitButton = oldDialogAddQuitButton(dialog, dialogDefinition.quitButton.doScoreScreen, dialogDefinition.quitButton.text, dialogDefinition.quitButton.hotkey)
        currentDialogButtons[player][quitButton] = dialogDefinition.quitButton
    end

    oldDialogDisplay(player, dialog, true)
end

---@param dialog DialogWrapper
---@param player player
function DialogQueue.Enqueue(dialog, player)
    dialogQueues[player]:insert(dialog)
    displayNextDialog(player)
end

---@param dialog DialogWrapper
---@param player player
function DialogQueue.EnqueueAfterCurrentDialog(dialog, player)
    dialogQueues[player]:insert(dialog, true)
end

---@param dialog DialogWrapper
---@param player player
function DialogQueue.Dequeue(dialog, player)

    if dialogQueues[player].n <= 0 then
        return
    end

    -- is currently displayed dialog being dequeued?
    if dialogQueues[player].next.value --[[@as DialogWrapper]] == dialog then
        DialogQueue.SkipCurrentDialog(player)
        return
    end

    for thisDialog in dialogQueues[player]:loop() do
        if thisDialog.value --[[@as DialogWrapper]] == dialog then
            thisDialog --[[@as LinkedListNode]]:remove()
            break
        end
    end
   
end

---@param player player
function DialogQueue.SkipCurrentDialog(player)
    playerQueueActive[player] = nil
    dialogQueues[player].next:remove()
    displayNextDialog(player)
end

local tableTriggerPlayer = {} ---@type table<trigger, player>
local tableTriggerDialog = {} ---@type table<trigger, DialogWrapper>
local tableTriggerButton = {} ---@type table<trigger, DialogButtonWrapper>

---@param trigger trigger
---@return player
function DialogQueue.GetTriggerPlayer(trigger)
    return tableTriggerPlayer[trigger]
end

---@param trigger trigger
---@return DialogWrapper
function DialogQueue.GetTriggerDialog(trigger)
    return tableTriggerDialog[trigger]
end

---@param trigger trigger
---@return DialogButtonWrapper
function DialogQueue.GetTriggerButton(trigger)
    return tableTriggerButton[trigger]
end

---@param trigger trigger
function DialogQueue.ClearEventResponsesForTrigger(trigger)
    tableTriggerPlayer[trigger] = nil
    tableTriggerDialog[trigger] = nil
    tableTriggerButton[trigger] = nil
end

function DialogQueue.NativeOverride()

    -- Event callbacks
    -- Decided to add special config option since this one is a bit more generic, and widely used instead of for just dialogs.
    if OVERRIDE_GET_TRIGGERING_PLAYER then
        GetTriggerPlayer = function()
            local triggerPlayer = oldGetTriggerPlayer()
            if oldGetTriggerPlayer() == nil then
                triggerPlayer = DialogQueue.GetTriggerPlayer(GetTriggeringTrigger())
            end

            return triggerPlayer
        end
    end

    GetClickedDialog = function()
        return DialogQueue.GetTriggerDialog(GetTriggeringTrigger())
    end

    GetClickedButton = function()
        return DialogQueue.GetTriggerButton(GetTriggeringTrigger())
    end

    -- Event registry
    ---@param trigger trigger
    ---@param dialog DialogWrapper
    TriggerRegisterDialogEvent = function(trigger, dialog)
        table.insert(dialog.triggers, trigger)
    end

    ---@param trigger trigger
    ---@param button DialogButtonWrapper
    TriggerRegisterDialogButtonEvent = function(trigger, button)
        table.insert(button.triggers, trigger)
    end

    -- Dialog API
    DialogCreate = DialogWrapper.create

    ---@param dialog DialogWrapper
    DialogDestroy = function(dialog)
        for player in SetUtils.getPlayersAll():elements() do
            DialogQueue.Dequeue(dialog, player)
        end
        dialog.buttons = {}
        dialog.messageText = nil
        dialog.quitButton = nil
        dialog.triggers = {}
    end
    DialogClear = DialogDestroy

    ---@param dialog DialogWrapper
    ---@param messageText string
    DialogSetMessage = function (dialog, messageText)
        dialog.messageText = messageText
    end

    ---@param dialog DialogWrapper
    ---@param buttonText string
    ---@param hotkey integer
    ---@return DialogButtonWrapper
    DialogAddButton = function(dialog, buttonText, hotkey)
        local button = DialogButtonWrapper.create()
        button.text = buttonText
        button.hotkey = hotkey
        table.insert(dialog.buttons, button)
        return button
    end

    ---@param dialog DialogWrapper
    ---@param doScoreScreen boolean
    ---@param buttonText string
    ---@param hotkey integer
    ---@return DialogButtonWrapper
    DialogAddQuitButton = function(dialog, doScoreScreen, buttonText, hotkey)
        local button = DialogQuitButtonWrapper.create()
        button.text = buttonText
        button.hotkey = hotkey
        button.doScoreScreen = doScoreScreen
        dialog.quitButton = button
        return button
    end
   
    ---@param player player
    ---@param dialog DialogWrapper
    ---@param show boolean
    DialogDisplay = function (player, dialog, show)
        if show then
            DialogQueue.Enqueue(dialog, player)
        else
            DialogQueue.Dequeue(dialog, player)
        end
    end

end

---@param triggers trigger[]
---@param player player
---@param dialog DialogWrapper
---@param button DialogButtonWrapper
local callTriggers = function(triggers, player, dialog, button)
    for _, trigger in ipairs(triggers) do
        if trigger ~= nil then
            tableTriggerPlayer[trigger] = player
            tableTriggerDialog[trigger] = dialog
            tableTriggerButton[trigger] = button

            if TriggerEvaluate(trigger) then
                TriggerExecute(trigger)
            end
        end
    end
end

function DialogQueue.Init()
    local trigger = CreateTrigger()

    for player in SetUtils.getPlayersAll():elements() do
        local dialog = oldDialogCreate() -- dialog per player
        playerDialog[player] = dialog
        dialogQueues[player] = LinkedList.create()
        oldTriggerRegisterDialogEvent(trigger, dialog)
    end

    TriggerAddAction(trigger, function ()
        local triggerPlayer = oldGetTriggerPlayer()
        local dialogNode = dialogQueues[triggerPlayer].next --[[@as LinkedListNode]]
        local dialog = dialogNode.value --[[@as DialogWrapper]]
        local actualButton = oldGetClickedButton()
        local button = currentDialogButtons[triggerPlayer][actualButton]

        callTriggers(dialog.triggers, triggerPlayer, dialog, button)
        callTriggers(button.triggers, triggerPlayer, dialog, button)

        if dialog.callback ~= nil then dialog:callback(triggerPlayer) end
        if button.callback ~= nil then button:callback(dialog, triggerPlayer) end

        local status, err = pcall(dialogNode.remove, dialogNode)
        if status ~= true then
            print("Failed removing Dialog Node from Linked List. This is caused by the DialogWrapper being Dequeued by the callback function. In the future, please avoid this!")
            print("Error: " .. err)
        end
        playerQueueActive[triggerPlayer] = nil
        displayNextDialog(triggerPlayer)
    end)
end

if OVERRIDE_NATIVES and OnInit then OnInit.root("DialogQueue", DialogQueue.NativeOverride) end
if OnInit then OnInit.final(function (require)
    require "SetUtils"
    DialogQueue.Init()
end) end
end
Lua:
do
--[[
    MultiOptionDialog v1.0 by Insanity_AI
    ------------------------------------------------------------------------------
    DialogQueue extension to handle dialogs with buttons which cycle options and commit button at the end.
    Each option has a previewFunc which can be called when that option is currently selected.
    dialog's callback function is called when the player has commited their options.

    Requires:
        DialogQueue - [INSERT LINK HERE]
        SetUtils - https://www.hiveworkshop.com/threads/set-group-datastructure.331886/

    Installation:
        Just Plug & Play, nothing specific to be done.
       
    MultiOptionDialog API:
        MultiOptionDialog.create(data?) -> creates a new MultiOptionDialog.
                                        -> data can be an existing MultiOptionDialog object to make another copy of.
        MultiOptionDialog.callback      - callback function for when MultiOptionDialog has been commited.
        MultiOptionDialog.title         - Title text for dialog. (nillable)
        MultiOptionDialog.buttons       - array of MultiOptionDialogButton objects (not nillable, must be at least empty table.)
        MultiOptionDialog.commitButton  - DialogButtonWrapper for commit.
        MultiOptionDialog:Enqueue(playerSet) -> enqueues MultiOptionDialog for Set of players.
        MultiOptionDialog:Dequeue(playerSet) -> dequeues MultiOptionDialog for Set of players.
   
    MultiOptionDialogButton API:
        MultiOptionDialogButton.create(data?)   -> data is an existing MultiOptionDialogButton object to make another copy of.
        MultiOptionDialogButton.messageFormat   - format string to merge button prefix and option name.
        MultiOptionDialogButton.hotkey          - button's hotkey (must be single char or nil)
        MultiOptionDialogButton.options         - array of MultiOptionDialogButtonOption to cycle through on button click. (not nillable, must be at least empty table)
        MultiOptionDialogButton.prefix          - button's prefix text (nillable)
   
    MultiOptionDialogButtonOption API:
        MultiOptionDialogButtonOption.create(data?)     -> data is an existing MultiOptionDialogButtonOption object to make another copy of.
        MultiOptionDialogButtonOption.previewCallback   - callback function called upon the option being cycled to by a button click.
        MultiOptionDialogButtonOption.name              - option name to be displayed on button.
]]

---@class MultiOptionDialog
---@field callback fun(dialog: MultiOptionDialog, player: player, buttonChosenOptionPairs: table<MultiOptionDialogButton, MultiOptionDialogButtonOption>)
---@field buttons MultiOptionDialogButton[]
---@field commitButton DialogButtonWrapper
---@field title string
MultiOptionDialog = {}
MultiOptionDialog.__index = MultiOptionDialog

---@param data? MultiOptionDialog
---@return MultiOptionDialog
function MultiOptionDialog.create(data)
    local instance = setmetatable({}, MultiOptionDialog)
    instance.buttons = {}
    if data ~= nil then
        for _, button in ipairs(data.buttons) do
            table.insert(instance.buttons, MultiOptionDialogButton.create(button))
        end

        instance.callback = data.callback
        instance.title = data.title
        instance.commitButton = DialogButtonWrapper.create(data.commitButton)
    end
    return instance
end

---@class MultiOptionDialogButton
---@field options MultiOptionDialogButtonOption[]
---@field prefix string
---@field messageFormat string
---@field hotkey string
MultiOptionDialogButton = {}
MultiOptionDialogButton.__index = MultiOptionDialogButton

---@param data? MultiOptionDialogButton
---@return MultiOptionDialogButton
function MultiOptionDialogButton.create(data)
    local instance = setmetatable({}, MultiOptionDialogButton)
    instance.options = {}
   
    if data ~= nil then
        for _, option in ipairs(data.options) do
            table.insert(instance.options, MultiOptionDialogButtonOption.create(option))
        end
        instance.messageFormat = data.messageFormat
        instance.prefix = data.prefix
        instance.hotkey = data.hotkey
    else
        instance.messageFormat = "[\x25s] \x25s"
    end
    return instance
end

---@class MultiOptionDialogButtonOption
---@field previewCallback fun(player: player)
---@field name string
MultiOptionDialogButtonOption = {}
MultiOptionDialogButtonOption.__index = MultiOptionDialogButtonOption

---@param data? MultiOptionDialogButtonOption
---@return MultiOptionDialogButtonOption
function MultiOptionDialogButtonOption.create(data)
    local instance = setmetatable({}, MultiOptionDialogButtonOption)
    if data ~= nil then
        instance.previewCallback = data.previewCallback
        instance.name = data.name
    end
    return instance
end

-- Internal Classes
---@class MultiDialogWrapper : DialogWrapper
---@field buttons MultiDialogButtonWrapper[]
---@field _dialogDef MultiOptionDialog

---@class MultiDialogButtonWrapper : DialogButtonWrapper
---@field _buttonDef MultiOptionDialogButton
---@field _option integer

---@class DialogPlayerData
---@field players Set
---@field playerDialogWrappers table<player, MultiDialogWrapper>

---@param button MultiDialogButtonWrapper
---@param dialog MultiDialogWrapper
---@param player player
local function cycleOption(button, dialog, player)
    button._option = button._option + 1
    local buttonDef = button._buttonDef
    if button._option > #buttonDef.options then
        button._option = 1
    end
    local option = buttonDef.options[button._option]
    option.previewCallback(player)
    button.text = string.format(buttonDef.messageFormat, buttonDef.prefix, option.name)
    DialogQueue.EnqueueAfterCurrentDialog(dialog, player)
end

local queuedDialogsForPlayers = {} ---@type table<MultiOptionDialog, DialogPlayerData>

---@param dialog MultiOptionDialog
---@param player player
local function dequeuePlayer(dialog, player)
    local playerDialogData = queuedDialogsForPlayers[dialog]
    playerDialogData.players:removeSingle(player)
    if playerDialogData.players:isEmpty() then
        queuedDialogsForPlayers[dialog] = nil
    end
end

---@param dialog MultiDialogWrapper
---@param player player
local function commitOptions(_, dialog, player)
    local selectedButtonOptions = {} ---@type table<MultiOptionDialogButton, MultiOptionDialogButtonOption>

    for _, thisButton in ipairs(dialog.buttons) do
        if thisButton._buttonDef then -- last button is not MultiOptionDialogButton, just a normal DialogButtonWrapper
            selectedButtonOptions[thisButton._buttonDef] = thisButton._buttonDef.options[thisButton._option]
        end
    end

    dialog._dialogDef.callback(dialog._dialogDef, player, selectedButtonOptions)
    dequeuePlayer(dialog._dialogDef, player)
end

---@param dialogDef MultiOptionDialog
---@return DialogWrapper
local function toDialogWrapper(dialogDef)

    local dialogWrapper = { ---@type DialogWrapper
        triggers = {},
        buttons = {},
        messageText = dialogDef.title,
    }

    for _, buttonDef in ipairs(dialogDef.buttons) do
        table.insert(dialogWrapper.buttons, {
            text = string.format(buttonDef.messageFormat, buttonDef.prefix, buttonDef.options[1].name),
            hotkey = string.byte(buttonDef.hotkey) or 0,
            triggers = {},
            callback = cycleOption,
            _buttonDef = buttonDef
        })
    end

    table.insert(dialogWrapper.buttons, {
        text = "Done",
        hotkey = 0,
        triggers = {},
        callback = commitOptions
    })

    return dialogWrapper
end

---@param players Set
---@return boolean success
function MultiOptionDialog:Enqueue(players)
    if queuedDialogsForPlayers[self] ~= nil then
        return false
    end
    local dialogWrapper = toDialogWrapper(self)
    local playerDialogData = {
        players = players,
        playerDialogWrappers = {}
    }
    queuedDialogsForPlayers[self] = playerDialogData

    for player in players:elements() do
        local playerDialog = DialogWrapper.create(dialogWrapper) --[[@as MultiDialogWrapper]]
        playerDialog._dialogDef = self

        for index, button in ipairs(playerDialog.buttons) do
            button._option = 1
            button._buttonDef = self.buttons[index]
        end
        playerDialogData.playerDialogWrappers[player] = playerDialog
        DialogQueue.Enqueue(playerDialog, player)
    end

    return true

end

---@param players Set
---@return boolean success
function MultiOptionDialog:Dequeue(players)

    if queuedDialogsForPlayers[self] ~= nil then
        return false
    end

    local playersToDequeue = Set.intersection(players, queuedDialogsForPlayers[self].players)

    queuedDialogsForPlayers[self].players:removeAll(playersToDequeue)
    for player --[[@as player]] in playersToDequeue:elements() do
        dequeuePlayer(self, player)
        DialogQueue.Dequeue(queuedDialogsForPlayers[self].playerDialogWrappers[player], player)
    end

    return true

end
end
Lua:
do
--[[
    VotingDialog v1.0 by Insanity_AI
    ------------------------------------------------------------------------------
    MultiOptionDialog extension to wrap them in a player voting logic.

    Requires:
        MultiOptionDialog - [INSERT LINK HERE]
        SetUtils - https://www.hiveworkshop.com/threads/set-group-datastructure.331886/

    Installation:
        Just Plug & Play, nothing specific to be done.
       
    MultiOptionDialog API:
        VotingDialog.create(data?)      -> creates a new VotingDialog.
                                        -> data can be an existing VotingDialog object to make another copy of.
        VotingDialog.votingDoneCallback - callback function for when all players have comitted their votes.
        VotingDialog.title              - Title text for dialog. (nillable)
        VotingDialog.buttons            - array of MultiOptionDialogButton objects (not nillable, must be at least empty table.)
        VotingDialog.commitButton       - DialogButtonWrapper for commit.
        VotingDialog:Enqueue(playerSet) -> enqueues VotingDialog for Set of players.
        VotingDialog:Dequeue(playerSet) -> dequeues VotingDialog for Set of players.
        MultiOptionDialog.callback is not to be used by end-user, it is used by VotingDialog to count votes!
]]

---@class VotingDialog : MultiOptionDialog
---@field votingDoneCallback fun(selectedOptions: MultiOptionDialogButtonOption[])
VotingDialog = {}
VotingDialog.__index = VotingDialog
setmetatable(VotingDialog, MultiOptionDialog)

---@param data? VotingDialog
---@return VotingDialog
VotingDialog.create = function(data)
    local instance = setmetatable(MultiOptionDialog.create(data), VotingDialog)

    if data ~= nil then
        instance.votingDoneCallback = data.votingDoneCallback
    end

    return instance --[[@as VotingDialog]]
end

-- jesus christ this datastructure.
local playersVotes = {} ---@type table<VotingDialog, {players : Set, buttonOptionCounts: table<MultiOptionDialogButton, table<MultiOptionDialogButtonOption, integer>>}>

---@param dialog VotingDialog
local function functionVotingFinish(dialog)
    local selectedOptions = {} ---@type MultiOptionDialogButtonOption[]

    for _, optionCountPairs in pairs(playersVotes[dialog].buttonOptionCounts) do
       
        local maxOption = nil
        local max = nil
        for option, count in pairs(optionCountPairs) do
            if max == nil or max < count then
                maxOption = option
                max = count
            end
        end

        table.insert(selectedOptions, maxOption)
    end

    playersVotes[dialog] = nil
    dialog.votingDoneCallback(selectedOptions)
end

---@param dialog VotingDialog
---@param player player
---@param buttonChosenOptionPairs table<MultiOptionDialogButton, MultiOptionDialogButtonOption>
local function processPlayerCommit(dialog, player, buttonChosenOptionPairs)

    local dataStruct = playersVotes[dialog]
    dataStruct.players:removeSingle(player)

    for button, option in pairs(buttonChosenOptionPairs) do
        if dataStruct.buttonOptionCounts[button] == nil then
            dataStruct.buttonOptionCounts[button] = {}
            dataStruct.buttonOptionCounts[button][option] = 1
        else
            dataStruct.buttonOptionCounts[button][option] = dataStruct.buttonOptionCounts[button][option] + 1
        end
    end

    if dataStruct.players:isEmpty() then
        functionVotingFinish(dialog)
    end

end

---@param players Set
---@return boolean success
function VotingDialog:Enqueue(players)

    if playersVotes[self] ~= nil then
        -- Already queued up!
        return false
    end

    local dataStruct = { ---@type {players : Set, buttonOptionCounts: table<MultiOptionDialogButton, table<MultiOptionDialogButtonOption, integer>>}
        players = players,
        buttonOptionCounts = {}
    }

    playersVotes[self] = dataStruct

    self.callback = processPlayerCommit
    MultiOptionDialog.Enqueue(self, players)

    return true
end

---@param players Set
---@return boolean success
function VotingDialog:Dequeue(players)

    if playersVotes[self] == nil then
        -- Not queued up!
        return false
    end

    playersVotes[self].players = playersVotes[self].players:removeAll(players)
    if playersVotes[self].players:isEmpty() then
        functionVotingFinish(self)
    end

    MultiOptionDialog:Dequeue(players);
    return true
end
end
 

Attachments

  • dialogTest.w3m
    114.7 KB · Views: 5
Last edited:
Status
Not open for further replies.
Top