• 🏆 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] DialogUtils

Level 4
Joined
Mar 25, 2021
Messages
18
Short snippet to display and handle dialogs more intuitively.

Features
  • Implement button-press handlers using Lua callbacks.
  • Create dialogs that can be shown (or hidden) to all or a specific player.
  • No dialog is actually created until you call the show()-method so you can define dialogs at any time you like and show them on demand.

Lua:
--[[
* DialogUtils 1.1.1 by Forsakn
*
* DialogUtils provides an easy way to predefine and show/hide native blizzard dialogs at will.
*
* API Functions:
**************************************************************
* The API only offers object-oriented notation. Colons are used after objects to access class-wide methods. Dots refer to attributes of the object itself, even when those are functions.
* ---------------
* | DialogUtils |
* ---------------
*        - This class at the time only has one purpose, to create new instances of the DialogData class.
*
*    DialogUtils.create(title)
*        - Returns a new DialogData instance
*        - Example:
*           local dialog = DialogUtils.create("DialogUtils test")
* --------------
* | DialogData |
* --------------
*        - This class holds on to dialog related data and provides functions for building/showing and hiding a native Blizzard dialog using that data.
*        - No static methods here, all methods must be called on an instance of DialogData, created via the DialogUtils.create method.
*
*    <DialogData>:addButton(text, callback, hotkeyId?)
*        - Adds a button to <DialogData>, this does not actually create a blizzard DialogButton but only adds data to the object for later use.
*        - The callback function has to provide one parameter, "player", this is the player who clicked the button.
*        - Returning true in the callback function will make the dialog stay open on click, useful for invalid choices.
*        - Example:
*           <DialogData>:addButton("button", function(player) print(GetPlayerName(player) end)
*
*    <DialogData>:show(player?)
*        - Builds and shows a blizzard Dialog adding all buttons added to <DialogData>.
*        - Internally the dialogs are indexed by player ID or -1 for a dialog thats shown to all players. Calling this function will destroy previous dialog at this index.
*        - Example:
*           <DialogData>:show() -- Shows the dialog to all players.
*           <DialogData>:show(Player(0)) -- Show the dialog only to red player.
*
*    <DialogData>:hide(player?)
*        - Hides the dialog created from calling <DialogData>:show from provided player.
*        - Example:
*           <DialogData>:hide() -- Hides the dialog from all players.
*           <DialogData>:hide(Player(0)) -- Hides the dialog from red player.
]]

do

    local dialogs = {}
    local dialogIdOccupier = {}
    local triggers = {}

    ----------------
    -- DialogData --
    ----------------

    ---@class dialogdata
    local DialogData = setmetatable({}, {})
    getmetatable(DialogData).__index = DialogData

    -- Define members (mostly to help with editor intellisense, will be overwritten on creation of DialogData)
    DialogData.title = ""
    DialogData.buttons = {}

    ---@param text string
    ---@param callback? function
    ---@param hotkeyId? integer
    function DialogData:addButton(text, callback, hotkeyId)
        table.insert(self.buttons, {text = text, callback = callback, hotkeyId = hotkeyId or 0})
    end

    ---@param player? player
    function DialogData:show(player)
        local dialogId = GetPlayerId(player) or -1
        player = player or GetLocalPlayer()

        -- Destroy old dialog and create new
        if dialogs[dialogId] then
            DialogDestroy(dialogs[dialogId])
        end
        dialogs[dialogId] = DialogCreate()
        local dialog = dialogs[dialogId]

        -- Destroy old trigger
        if triggers[dialogId] then
            DestroyTrigger(triggers[dialogId])
        end

        -- Create new trigger and add buttons to dialog
        local t = CreateTrigger()
        TriggerRegisterDialogEvent(t, dialog)
        for i = 1, #self.buttons do
            local button = self.buttons[i]
            local dialogButton = DialogAddButton(dialog, button.text, button.hotkeyId)
            TriggerAddCondition(t, Condition(function ()
                if GetClickedButton() == dialogButton and button.callback then
                    local clickingPlayer = GetTriggerPlayer()
                    local keepAlive = button.callback(clickingPlayer) == true
                    DialogDisplay(clickingPlayer, dialog, keepAlive)
                end
            end))
        end
        triggers[dialogId] = t

        -- Display dialog to player
        DialogSetMessage(dialog, self.title)
        DialogDisplay(player, dialog, true)

        -- Set self as occupier of this dialog (Used for hiding)
        dialogIdOccupier[dialogId] = self
    end

    ---@param player? player
    function DialogData:hide(player)
        player = player or GetLocalPlayer()
        for id, data in ipairs(dialogIdOccupier) do
            if data == self then
                local dialog = dialogs[id]
                if dialog then
                    if GetLocalPlayer() == player then
                        DialogDisplay(player, dialog, false)
                    end
                end
            end
        end
    end

    -----------------
    -- DialogUtils --
    -----------------

    DialogUtils = {}

    ---@param title string
    ---@return dialogdata
    function DialogUtils.create(title)
        local dialogData = setmetatable({}, getmetatable(DialogData))
        dialogData.title = title
        dialogData.buttons = {}
        return dialogData
    end

end

This is the result of the nested example.

dialogutils.gif


Lua:
-- Example display dialog to all players
TimerStart(CreateTimer(), 2, false , function ()
    DestroyTimer(GetExpiredTimer())

    local dialog = DialogUtils.create("DialogUtils test")
    dialog:addButton("Yes", function (player)
        print(string.format("\x25s clicked yes", GetPlayerName(player)))
    end)
    dialog:addButton("Other", function (player)
        print(string.format("\x25s clicked other", GetPlayerName(player)))
    end)
    dialog:addButton("Keep alive", function (player)
        print(string.format("\x25s clicked keep alive", GetPlayerName(player)))
        return true -- Returning true keeps the dialog alive instead of closing it.
    end)
    dialog:addButton("Close")
    dialog:show()

    -- This won't work in singleplayer since dialogs pause the game
    TimerStart(CreateTimer(), 5, false, function ()
        DestroyTimer(GetExpiredTimer())
        dialog:hide() -- Can hide dialog from specific player here if you wish.
    end)
end)

-- Example display dialog to specific player
TimerStart(CreateTimer(), 2, false , function ()
    DestroyTimer(GetExpiredTimer())

    local player = Player(0)

    local dialog = DialogUtils.create("DialogUtils test")
    dialog:addButton("Yes", function (player)
        print(string.format("\x25s clicked yes", GetPlayerName(player)))
    end)
    dialog:addButton("Other", function (player)
        print(string.format("\x25s clicked other", GetPlayerName(player)))
    end)
    dialog:addButton("Keep alive", function (player)
        print(string.format("\x25s clicked keep alive", GetPlayerName(player)))
        return true -- Returning true keeps the dialog alive instead of closing it.
    end)
    dialog:addButton("Close")
    dialog:show(player)

    -- This won't work in singleplayer since dialogs pause the game
    TimerStart(CreateTimer(), 5, false, function ()
        DestroyTimer(GetExpiredTimer())
        dialog:hide(player) -- Can skip supplying player here, but it's best to supply one since we have it available.
    end)
end)

-- Example nested dialogs
TimerStart(CreateTimer(), 2, false , function ()
    DestroyTimer(GetExpiredTimer())

    local dialog = DialogUtils.create("DialogUtils test")
    local dialog2 = DialogUtils.create("DialogUtils test 2")

    dialog:addButton("Go to dialog 2", function (player)
        dialog2:show(player)
    end)
    dialog:addButton("Close")

    dialog2:addButton("Ok")
    dialog2:addButton("Go back", function (player)
        dialog:show(player)
    end)

    dialog:show()
end)

Installation
  1. Copy the code in the "DialogUtils.lua"-spoiler and paste it into a custom script in your trigger editor.
  2. Finished! To get started I do however recommend you to copy an example from the examples-spoiler and place it in a custom script below the one you created in step 1. Edit and test it!

Changelog:
(2021-04-12) Version 1.1.1 introduced OO approach and adjustments made for dialog buttons showing another dialog, example is in spoiler.
(2021-04-14) Small code adjustments and added comments using @Eikonium's formatting from his Set library.
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
477
Hey Forsakn, thanks for sharing your snippet :)

Some random bit of feedback:
  • Would you mind adding a few screenshots of the resulting dialogs? This would help people like me to access the purpose of your snippet more quickly. Sure, dialogs are a known thing to most mappers, but one can see immediately, if this is just supposed to help with standard dialog stuff or if there is a special twist about it.
  • Would you mind explaining your API on top of your snippet, so that the user doesn't have to read through your code? Sure, the snippet is short enough, but that would increase the usability for less experienced users IMO. A bit of documentation, even for short snippets like this, helps grasping, what a piece of code is actually capable of doing and if it suits my personal needs ;)
  • You probably know that the letter sequence %s crashes the world editor upon Syntax Check (which happens during saving), when being part of any code snip in any script file, even inside of comments. Yes, you can avoid that crash by typing %%s instead (and WE requires that anyway), but that makes your code non-executable in other external Lua consoles (which require %s to work). Also, coding with %%s always puts you at risk of forgetting one %, crashing your WE and losing unsaved work.
    My personal best practice is to always code with \x25s instead. "\x25" is hex for a single percent sign, never crashes and works in both WE and external Lua consoles.
  • hotkeyId = hotkeyId and hotkeyId or 0 is equivalent to hotkeyId = hotkeyId or 0 (same for hideplayer).
  • You don't need to create new locals for applying default values on optional parameters. E.g. in the hide-method, you might just write player = player or GetLocalPlayer() instead of creating the hideplayer local.
  • Might be standard dialog knowledge, but I wonder, why you are rebuilding the whole dialogue in the show-method. Can't a dialog be recycled and shown again after use? Why does it have to be rebuilt?
 
Level 4
Joined
Mar 25, 2021
Messages
18
Hello @Eikonium and thank you for the feedback. :)

  • Would you mind adding a few screenshots of the resulting dialogs? This would help people like me to access the purpose of your snippet more quickly. Sure, dialogs are a known thing to most mappers, but one can see immediately, if this is just supposed to help with standard dialog stuff or if there is a special twist about it.
Will do!

  • Would you mind explaining your API on top of your snippet, so that the user doesn't have to read through your code? Sure, the snippet is short enough, but that would increase the usability for less experienced users IMO. A bit of documentation, even for short snippets like this, helps grasping, what a piece of code is actually capable of doing and if it suits my personal needs ;)
Will do! :D

  • You probably know that the letter sequence %s crashes the world editor upon Syntax Check (which happens during saving), when being part of any code snip in any script file, even inside of comments. Yes, you can avoid that crash by typing %%s instead (and WE requires that anyway), but that makes your code non-executable in other external Lua consoles (which require %s to work). Also, coding with %%s always puts you at risk of forgetting one %, crashing your WE and losing unsaved work.
    My personal best practice is to always code with \x25s instead. "\x25" is hex for a single percent sign, never crashes and works in both WE and external Lua consoles.
Ahh thanks for the tip!

  • hotkeyId = hotkeyId and hotkeyId or 0 is equivalent to hotkeyId = hotkeyId or 0 (same for hideplayer).
Oh pretty new to Lua so wanted to be as explicit as possible, thanks.

  • You don't need to create new locals for applying default values on optional parameters. E.g. in the hide-method, you might just write player = player or GetLocalPlayer() instead of creating the hideplayer local.
This I know, I just really don't like reassigning parameters, a habit from other languages, might change it.

  • Might be standard dialog knowledge, but I wonder, why you are rebuilding the whole dialogue in the show-method. Can't a dialog be recycled and shown again after use? Why does it have to be rebuilt?
I did recycle them at first, but that way if the handle of a new dialog was the same as the one already open for a player when showing the new dialog, the new one would not show (kind of difficult to explain). There's possibly another solution but I haven't found one this far. It's a pretty rare use case though so maybe I should add some kind of parameter for dialogs that might send you to another dialog, and recycle dialogs by default.
 
Level 4
Joined
Mar 25, 2021
Messages
18
I really like this resource, and I love that you added a video. Could you please elaborate on the steps needed to implement this, and why one of the addButton methods is returning true in your example? I just feel like a few additional comments/ some installation protocol would be beneficial.
Thanks! :smile:

I updated the post with installation instructions and added comments in the examples for the returns true as well.
 
Top