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

Level 24
Joined
Jun 26, 2020
Messages
1,850
Hello, I made this system to directly get the syncronized value of a function that returns a async value without the necessity of events:
Requires:
Lua:
if Debug then Debug.beginFile("GetSyncedData") end
OnInit("GetSyncedData", function ()
    Require "Obj2Str" -- https://www.hiveworkshop.com/pastebin/65b5fc46fc82087ba24609b14f2dc4ff.25120

    local PREFIX = "GetSyncedData__SYNC"
    local END_PREFIX = "GetSyncedData__END_SYNC"
    local BLOCK_LENGHT = 246

    local LocalPlayer = GetLocalPlayer()
    local callbacks = {} ---@type (fun(noSync: boolean?): thread)[]
    local actString = ""
    local actThread = nil ---@type thread
    local areWaiting = {} ---@type table<thread, thread[]>
    local areSyncs = false

    ---@param s string
    local function Sync(s)
        while true do
            local sub = s:sub(1, BLOCK_LENGHT)
            s = s:sub(BLOCK_LENGHT + 1)
            if s:len() > 0 then
                BlzSendSyncData(PREFIX, sub)
            else
                BlzSendSyncData(END_PREFIX, sub)
                break
            end
        end
    end

    ---Syncs the value of the player of the returned value of the given function,
    ---you can also pass the parameters of the function.
    ---
    ---Or you can pass an array (table) with various functions to sync and the next
    ---index of each function should be an array (table) with its arguments, and then the returned
    ---value is an array (table) with the results in the order you set them.
    ---
    ---The sync takes time, so the function yields the thread until the data is synced.
    ---@async
    ---@generic T
    ---@param p player
    ---@param func fun(...): T || table
    ---@vararg any
    ---@return T | table
    function GetSyncedData(p, func, ...)
        if not (GetPlayerController(p) == MAP_CONTROL_USER and GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING) then
            error("The player " .. GetPlayerName(p) .. " is not an in-game player.", 2)
        end

        local data = ""
        if type(func) == "function" then
            if p == LocalPlayer then
                data = Obj2Str(func(...))
            end
        elseif type(func) == "table" then
            if p == LocalPlayer then
                local result = {}
                for i = 1, #func, 2 do
                    table.insert(result, func[i](func[i+1] and table.unpack(func[i+1])))
                end
                data = Obj2Str(result)
            end
        else
            error("Invalid parameter", 2)
        end

        local t = coroutine.running()

        table.insert(callbacks, function (noSync)
            if not noSync and p == LocalPlayer then
                Sync(data)
            end
            return t
        end)

        if #callbacks == 1 then
            actThread = callbacks[1]()
        end

        areSyncs = true
        local success, value = coroutine.yield() ---@type boolean, T | table

        if not success then
            error("Error during the conversion", 2)
        end

        return value
    end

    ---Yields the thread until last sync called from `GetSyncedData` ends.
    ---
    ---It does nothing if there isn't a queued sync from `GetSyncedData` or is the thread that function yielded or is yielded for another reason.
    function WaitLastSync()
        if areSyncs then
            local thr = coroutine.running()
            local last = callbacks[#callbacks](true)
            if last == thr or coroutine.status(thr) == "suspended" then
                return
            end
            if not areWaiting[last] then
                areWaiting[last] = {}
            end
            table.insert(areWaiting[last], thr)
            coroutine.yield(thr)
        end
    end

    local t = CreateTrigger()
    for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
        BlzTriggerRegisterPlayerSyncEvent(t, Player(i), PREFIX, false)
        BlzTriggerRegisterPlayerSyncEvent(t, Player(i), END_PREFIX, false)
    end
    TriggerAddAction(t, function ()
        actString = actString .. BlzGetTriggerSyncData()
        if BlzGetTriggerSyncPrefix() == END_PREFIX then
            table.remove(callbacks, 1)

            local thre = nil
            if #callbacks > 0 then
                thre = callbacks[1]()
            else
                areSyncs = false
            end

            coroutine.resume(actThread, pcall(Str2Obj, actString))

            if areWaiting[actThread] then
                for _, thr in ipairs(areWaiting[actThread]) do
                    coroutine.resume(thr)
                end
                areWaiting[actThread] = nil
            end

            actThread = thre or actThread -- Is nil if there are no more callbacks
            actString = ""
        end
    end)
end)
if Debug then Debug.endFile() end

Usage
To get information of the camera of a player:
Lua:
-- Prints to all players the camera target distance of the Player 0.
print(GetSyncedData(Player(0), GetCameraField, CAMERA_FIELD_TARGET_DISTANCE))
Maybe you wanna sync various values, and for that you can sync them individually, but every sync has a delay and they will be acumulated, to sync various values in one take you should pass a table that contains every function and after their indexes should be a table with the passed parameters (you can use nil for no passing any parameter), example:
Lua:
local data = GetSyncedData(p, {
    GetCameraBoundMaxX, nil,
    GetCameraBoundMaxY, nil,
    GetCameraBoundMinX, nil,
    GetCameraBoundMinY, nil,
 
    GetCameraField, {CAMERA_FIELD_ANGLE_OF_ATTACK}
})
It returns a table with the returned values in the order you set them:
Lua:
print("Bounds: " .. table.concat(data, ", ", 1, 4))
print("Angle of attack: " .. data[5])
And if the passed function returns a lua table, it will return a copy of that lua table:
Lua:
local data = GetSyncedData(Player(0), os.date, "*t")
for k, v in pairs(data) do
    print(k, v) -- print the osdate of the Player Red
end
In case you are running the sync in a created thread (like running a trigger with another trigger) and you wanna also yield the first thread, use the function WaitLastSync
Lua:
local data

local testThread = CreateTrigger()
TriggerAddAction(testThread, function ()
    data = GetSyncedData(Player(0), os.date, "*t")
end)

local t = CreateTrigger()
TriggerRegisterPlayerChatEvent(t, Player(0), "-wait", true)
TriggerAddAction(t, function ()
    data = nil
    TriggerExecute(testThread)
    WaitLastSync() -- without this line, this thread won't be yielded
    for k, v in pairs(data) do
        print(k, v)
    end
end)

WARNING: Trying to sync handles like units or frames could lead to errors that lead to desyncs, because they don't have the same handle id between players.

Pls give feedback.

Test map (Test it in multiplayer or multi-instance to look better the results):
 

Attachments

  • GetSyncedData.w3m
    93.8 KB · Views: 5
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Hi Herly,

Your Object2String/String2Object should be its own submitted resource, or a dual part of this one. It needs to be reviewed, as it raises some questions:
  • It stores booleans as either true or false but only loads them as true.
  • You create a new string to store strings instead of just storing the strings, which I don't think is an issue to just let them be.
  • You create a new table instead of referencing the table. This would only make sense with a save/load system.
  • Storing synced functions by their return value.
  • Why is this limited to FogState typecasting?
@Eikonium has a SyncedTable resource which seems to me to be the sane approach for syncing table data, rather than a mountainous string: [Lua] - SyncedTable. It also allows the table to be preserved, rather than creating a new one.

I get the feeling that if you just used SyncedTable to store everything for all players, instead of all the complicated string hacks, you would get the same desired end result.
 
Level 24
Joined
Jun 26, 2020
Messages
1,850
Hello Bribe.
Your Object2String/String2Object should be its own submitted resource, or a dual part of this one.
Its very uncomplete, and I was hoping that somebody else do a better one, because I'm not very good with "encrypting".
It stores booleans as either true or false but only loads them as true.
What?
You create a new string to store strings instead of just storing the strings, which I don't think is an issue to just let them be.
You create a new table instead of referencing the table. This would only make sense with a save/load system.
Storing synced functions by their return value.
Honestly, I don't get it.
Why is this limited to FogState typecasting?
I'm using the typecast hack used in Jass Typecasting
@Eikonium has a SyncedTable resource which seems to me to be the sane approach for syncing table data, rather than a mountainous string: [Lua] - SyncedTable. It also allows the table to be preserved, rather than creating a new one.

I get the feeling that if you just used SyncedTable to store everything for all players, instead of all the complicated string hacks, you would get the same desired end result.
I don't know what has to do the SyncedTable with my system, that is for when you wanna iterate in a table it does in the same order for all players, when my system is for directly send and get the local value of a player to the server.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I guess I have no idea what this is doing or why you would do this. It's not an area I've explored in the past.

I'll backtrack on what I said about the booleans:

elseif typ == "boolean" then
return value == "true"

That's fine. I read this first thing in the morning and thought you were just returning "true".
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I was having an issue here and here I wanted to solve it and share my results.
Thanks! Having background for what led to a resource's existence is critical for me to wrap my head around it. Now it makes a lot more sense why things look so complex - lots of trial and error to get here.

If you want to have "encrypting", I would recommend looking at some kind of string compressor like this: GitHub - Rochet2/lualzw: A relatively fast LZW compression algorithm in pure lua
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I would recommend updating the API to something like this:

Lua:
OnInit(function()        -- https://www.hiveworkshop.com/threads/global-initialization.317099/
    Require "LinkedList" -- https://www.hiveworkshop.com/threads/definitive-doubly-linked-list.339392/
    Require "Obj2Str"    -- https://www.hiveworkshop.com/pastebin/65b5fc46fc82087ba24609b14f2dc4ff.25120

    local PREFIX = "SYNC"

    local threads = LinkedList.create()

    ---@param co thread
    local function EnqueueThread(co)
        threads:insert(co)
    end

    ---@return thread
    local function DequeueThread()
        local node = threads:getNext() ---@type LinkedListNode
        local co = node.value
        node:remove()
        return co
    end

    ---Syncs the value of the player of the returned value of the given function,
    ---you can also pass the parameters of the function.
    ---
    ---Or you can pass an array (table) with various functions to sync and the next
    ---index of each function should be an array (table) with its arguments, and then the returned
    ---value is an array (table) with the results in the order you set them.
    ---
    ---The sync takes time, so the function yields the thread until the data is synced.
    ---
    ---Be careful, because if the returned value converted to string has a length greater
    ---than 247 the system returns an error.
    ---@generic T
    ---@param p player
    ---@param func fun(...?: any): T || table
    ---@param ...? any
    ---@return T | table
    function GetSyncedData(p, func, ...)
        if p == GetLocalPlayer() then
            if type(func) == "function" then
                BlzSendSyncData(PREFIX, Obj2Str(func(...)))
            elseif type(func) == "table" then
                local result = {}
                for i = 1, #func, 2 do
                    table.insert(result, func[i](table.unpack(func[i+1])))
                end
                BlzSendSyncData(PREFIX, Obj2Str(result))
            else
                error("Invalid parameter", 2)
            end
        end

        EnqueueThread(coroutine.running())

        local success, value = coroutine.yield()

        if not success then
            error("Error during the conversion", 2)
        end

        return value
    end

    OnInit.final(function ()
        local t = CreateTrigger()
        ForForce(bj_FORCE_ALL_PLAYERS, function ()
            BlzTriggerRegisterPlayerSyncEvent(t, GetEnumPlayer(), PREFIX, false)
        end)
        TriggerAddAction(t, function ()
            coroutine.resume(DequeueThread(), pcall(Str2Obj, BlzGetTriggerSyncData()))
        end)
    end)
end)

To get around the max-string-length limitation, I can only think of using multiple calls to the Blizzard natives and piece together the results once completed.
 
Level 8
Joined
Jan 23, 2015
Messages
121
Hi, nice coroutines usage!

I wonder if you really need to use queue though. Assuming that I understood the code correctly, if I run something like foreach(player p) print(GetPlayerId(p), GetSyncedData(p, GetPlayerId, p)), it will wait each player's sync before iterating over to the next player, and the queue doesn't grow more than one element in size, so it's redundant. Well, in case in can never be runned simultaneously.

But I think if I run a timer or a trigger for each player with same action as above, I doubt they will wait for one another, and then the queue maybe won't dispatch coroutines correctly, since each player's internet and other stuff should mess up the order in which sync events are triggered. That's just hand-waving though, maybe you could test this theory out?
 
Level 24
Joined
Jun 26, 2020
Messages
1,850
Hi, nice coroutines usage!

I wonder if you really need to use queue though. Assuming that I understood the code correctly, if I run something like foreach(player p) print(GetPlayerId(p), GetSyncedData(p, GetPlayerId, p)), it will wait each player's sync before iterating over to the next player, and the queue doesn't grow more than one element in size, so it's redundant. Well, in case in can never be runned simultaneously.

But I think if I run a timer or a trigger for each player with same action as above, I doubt they will wait for one another, and then the queue maybe won't dispatch coroutines correctly, since each player's internet and other stuff should mess up the order in which sync events are triggered. That's just hand-waving though, maybe you could test this theory out?
Thanks and the queue is necessary, because that considers if the GetSyncedData is used in different threads at the same time.
PD: I don't know if this can be used in a ForForce callback since that creates a new thread that is not lua coroutine, unless someone creates a system to do that.
 
Level 8
Joined
Jan 23, 2015
Messages
121
Well, the threads are executed consecutive to one another. But yeah, they await for the synced data. That's what I'm curious about, how do you ensure that the Sync Trigger (data receive) events would be in the same order that different players did send it near-simultaneously? ForForce or not, if you managed to get P1's then P2's then P3's in the queue, can't there be a situation that P1 is lagging and P2's or P3's data comes and triggers the event before, thus dequeuing P1's thread, which is not the correct thread? Remember that since BlzSendSyncData is protected by if localPlayer, the game cant distinguish which one of the three syncs is "first"
 
Level 24
Joined
Jun 26, 2020
Messages
1,850
Well, the threads are executed consecutive to one another. But yeah, they await for the synced data. That's what I'm curious about, how do you ensure that the Sync Trigger (data receive) events would be in the same order that different players did send it near-simultaneously? ForForce or not, if you managed to get P1's then P2's then P3's in the queue, can't there be a situation that P1 is lagging and P2's or P3's data comes and triggers the event before, thus dequeuing P1's thread, which is not the correct thread? Remember that since BlzSendSyncData is protected by if localPlayer, the game cant distinguish which one of the three syncs is "first"
Basically I'm asumming a lot of things, I assume that all the syncs lasts the same so the first that syncs is the first the ends to sync (that's why I'm using a queue), and I'm asumming that even in case of lag, it will have the same rules as when a player do a normal action, because they also are being synced.
Maybe this is incorrect, but after seeing that is very similar in how it works in the wurst standard library and those guys have much more experience than me, I think is correct, unless you show me the opposite.

Edit: Looking again the wurst library, it seems that it does the next sync right after the current sync finished and not expecting that it should happen right after the previous, so I think I have to change it in mine.
 
Last edited:
Level 24
Joined
Jun 26, 2020
Messages
1,850
I am curious to know your methodology for testing this. The code itself looks mostly fine, but I don't know what the best way to actually trigger these scenarios is. Wouldn't one need to run multiple instances of WC3?
Tbh I didn't test it in multi-instances from long time ago, I just casually find errors when I use it in my maps and then trying to think any possible scenario and test it in them and edit it until I get no errors.
 
Level 24
Joined
Jun 26, 2020
Messages
1,850
Very well, recently I tested again with multi-instance, it seems that at least in the example of "table" I made the mistake of creating a location locally.
This is how it looked:
Lua:
Debug.beginFile("test table")
OnInit(function ()
    Require "GetSyncedData"

    local function GetTable()
        return {
            true,
            [{1}] = "asas",
            tab = {0},
            [{"a5"}] = {"a6", "a7"},
            point = Location(0,0)
        }
    end

    local t = CreateTrigger()
    TriggerRegisterPlayerChatEvent(t, Player(0), "-table", true)
    TriggerAddAction(t, function ()
        print(Obj2Str(GetSyncedData(Player(0), GetTable)))
    end)

end)
Debug.endFile()

This is how it should look like:
Lua:
Debug.beginFile("test table")
OnInit(function ()
    Require "GetSyncedData"

    local l

    local function GetTable()
        return {
            true,
            [{1}] = "asas",
            tab = {0},
            [{"a5"}] = {"a6", "a7"},
            point = l
        }
    end

    local t = CreateTrigger()
    TriggerRegisterPlayerChatEvent(t, Player(0), "-table", true)
    TriggerAddAction(t, function ()
        l = Location(0,0)
        print(Obj2Str(GetSyncedData(Player(0), GetTable)))
    end)

end)
Debug.endFile()
Anyway, is still dangerous try to sync handles in Lua, because they are not the same between players and can lead to errors.
 

Uncle

Warcraft Moderator
Level 64
Joined
Aug 10, 2018
Messages
6,510
Very well, recently I tested again with multi-instance, it seems that at least in the example of "table" I made the mistake of creating a location locally.
This is how it looked:
Lua:
Debug.beginFile("test table")
OnInit(function ()
    Require "GetSyncedData"

    local function GetTable()
        return {
            true,
            [{1}] = "asas",
            tab = {0},
            [{"a5"}] = {"a6", "a7"},
            point = Location(0,0)
        }
    end

    local t = CreateTrigger()
    TriggerRegisterPlayerChatEvent(t, Player(0), "-table", true)
    TriggerAddAction(t, function ()
        print(Obj2Str(GetSyncedData(Player(0), GetTable)))
    end)

end)
Debug.endFile()

This is how it should look like:
Lua:
Debug.beginFile("test table")
OnInit(function ()
    Require "GetSyncedData"

    local l

    local function GetTable()
        return {
            true,
            [{1}] = "asas",
            tab = {0},
            [{"a5"}] = {"a6", "a7"},
            point = l
        }
    end

    local t = CreateTrigger()
    TriggerRegisterPlayerChatEvent(t, Player(0), "-table", true)
    TriggerAddAction(t, function ()
        l = Location(0,0)
        print(Obj2Str(GetSyncedData(Player(0), GetTable)))
    end)

end)
Debug.endFile()
Anyway, is still dangerous try to sync handles in Lua, because they are not the same between players and can lead to errors.
Hello, would you be kind enough to show me how to sync the os.date function? Can I even use this method or is it dangerous?

Also, forgive me if this is too offtopic but I'm dealing with desyncs myself and wondering if I'm using unsafe methods here. I'm creating these "classes" for each player to store data to them. Originally I was using [player] as the index but I heard that it wasn't safe so I'm now using their player id:
Lua:
    function SaveLoadUI:Create(player)
        local pn = GetConvertedPlayerId(player)
        if SaveLoadUIList[pn] ~= nil then return end

        local o = {}
        setmetatable(o, self)
        self.__index = self
    
        o.player = player
        o.playerNumber = pn
        o.background = nil
        o.title = nil
        o.closeButton = nil
        o.scrollbar = nil
        o.isVisible = false
        o.saveFileButtons = {}
        o.saveFileIcons = {}
        o.saveFileText = {}
        o.saveFileCount = 0
        o.saveData = {}
        o.saveName = {}
        o.canLoad = true
    
        SaveLoadUIList[pn] = o
        return o
    end
Here's an example of how I put these SaveLoadUI "classes" to use:
Lua:
    function SaveLoadUI:Show(flag)
        self.isVisible = flag

        if flag then
            if GetLocalPlayer() == self.player then
                BlzFrameSetVisible(self.background, true)
            end
        else
            BlzFrameSetVisible(self.background, false)
        end
    end
I hope this isn't prone to desyncs. Thank you for any help!
 
Level 24
Joined
Jun 26, 2020
Messages
1,850
Hello, would you be kind enough to show me how to sync the os.date function? Can I even use this method or is it dangerous?
There is an example in the post, the function can sync almost any value: numbers, strings, booleans, some W3 handles (but this is dangerous, maybe I should change the method) and lua tables.
I'm dealing with desyncs myself and wondering if I'm using unsafe methods here. I'm creating these "classes" for each player to store data to them. Originally I was using [player] as the index but I heard that it wasn't safe so I'm now using their player id:
I don't see why there could be a desync, you are not changing a value that affect the outcome of the game, at least in your example, but what I do with frames is just using the same set of frames for all players and just editing their visibility for the local player, even when I have to store data only relevant for the local player I just use single variables without even using an array, even I can store different values in a lua table for each player without a desync:

For example this is a system I made for custom hotkeys that use save system (look the parts I use the LocalPlayer variable):
Lua:
Debug.beginFile("Hotkeys")
OnInit("Hotkeys", function ()
    Require "PlayerUtils"
    Require "FrameLoader"
    Require "Menu"
    Require "SaveHelper"

    local OskeyMeta = {
        [0] = "",
        "SHIFT",
        "CTRL",
        "CTRL + SHIFT",
        "ALT",
        "ALT + SHIFT",
        "ALT + CTRL",
        "ALT + SHIFT + CTRL",
        "META",
        "META + SHIFT",
        "META + CTRL",
        "META + CTRL + SHIFT",
        "META + ALT",
        "META + ALT + SHIFT",
        "META + ALT + CTRL",
        "META + ALT + SHIFT + CTRL"
    }

    local MAX_KEYS = 0xFF

    ---@param key oskeytype
    ---@return boolean
    local function IsBannedKey(key)
        if key == OSKEY_LALT then
            return true
        elseif key == OSKEY_RALT then
            return true
        elseif key == OSKEY_RCONTROL then
            return true
        elseif key == OSKEY_LCONTROL then
            return true
        elseif key == OSKEY_RSHIFT then
            return true
        elseif key == OSKEY_LSHIFT then
            return true
        elseif key == OSKEY_RMETA then
            return true
        elseif key == OSKEY_LMETA then
            return true
        end
        return false
    end

    local oskeyName = {} ---@type table<oskeytype, string>
    local oskeyConverted = {} ---@type table<integer, oskeytype>

    -- These values are different for each player
    local LocalPlayer = GetLocalPlayer()
    local selectingKey = false
    local frames = {} ---@type table<integer, framehandle>
    local referenceFrame = {} ---@type table<framehandle, integer>
    local hotkeyText = {} ---@type table<integer, framehandle>
    local frameSelected = -1
    local frameWithKey = {} ---@type table<oskeytype, table<integer, integer>>
    local edits = {} ---@type table<oskeytype, table<integer, integer>>
    local referencePair = {} ---@type table<integer, {[1]: oskeytype, [2]: integer}>

    local HotkeyButton = nil ---@type framehandle
    local BackdropHotkeyButton = nil ---@type framehandle
    local HotkeyMenu = nil ---@type framehandle
    local HotkeyMessage = nil ---@type framehandle
    local HotkeyBackpackSubMenu = nil ---@type framehandle
    local HotkeyBackpack = nil ---@type framehandle
    local HotkeyExit = nil ---@type framehandle
    local HotkeySave = nil ---@type framehandle
    local HotkeyYourDigimons = nil ---@type framehandle
    local HotkeyYourDigimonsSubMenu = nil ---@type framehandle

    local visibleMenu = nil ---@type framehandle

    local function UpdateHotkeys()
        for _, v in pairs(hotkeyText) do
            BlzFrameSetText(v, "")
        end
        for key, list in pairs(frameWithKey) do
            for meta, id in pairs(list) do
                BlzFrameSetText(hotkeyText[id], ((meta ~= 0) and (OskeyMeta[meta] .. " + ") or "") .. oskeyName[key])
            end
        end
    end

    local function SetHotkey()
        if GetTriggerPlayer() == LocalPlayer then
            local frame = BlzGetTriggerFrame()
            local id = referenceFrame[frame]
            if id and frames[id] then
                selectingKey = true
                frameSelected = id
                BlzFrameSetText(HotkeyMessage, "|cffFFCC00Press a key to set the hotkey|r")
            else
                BlzFrameSetText(HotkeyMessage, "|cffFF0000Error|r")
            end
        end
    end

    local trig = CreateTrigger()

    ---@param frame framehandle
    ---@param id integer
    local function AsingHotkey(frame, id)
        BlzTriggerRegisterFrameEvent(trig, frame, FRAMEEVENT_CONTROL_CLICK)
        TriggerAddAction(trig, SetHotkey)

        referenceFrame[frame] = id

        local text = BlzCreateFrameByType("TEXT", "name", frame, "", 0)
        BlzFrameSetAllPoints(text, frame)
        BlzFrameSetText(text, "")
        BlzFrameSetScale(text, 1)
        BlzFrameSetEnable(text, false)
        BlzFrameSetTextAlignment(text, TEXT_JUSTIFY_CENTER, TEXT_JUSTIFY_MIDDLE)
        BlzFrameSetLevel(text, 4)
        hotkeyText[id] = text
    end

    local function HotkeyBackpackFunc()
        if GetTriggerPlayer() == LocalPlayer then
            BlzFrameSetVisible(visibleMenu, false)
            BlzFrameSetVisible(HotkeyBackpackSubMenu, true)
            visibleMenu = HotkeyBackpackSubMenu
        end
    end

    local function HotkeyYourDigimonsFunc()
        if GetTriggerPlayer() == LocalPlayer then
            BlzFrameSetVisible(visibleMenu, false)
            BlzFrameSetVisible(HotkeyYourDigimonsSubMenu, true)
            visibleMenu = HotkeyYourDigimonsSubMenu
        end
    end

    local function HotkeyExitFunc()
        if GetTriggerPlayer() == LocalPlayer then
            BlzFrameSetVisible(HotkeyMenu, false)
            BlzFrameSetVisible(visibleMenu, false)
            BlzFrameSetEnable(HotkeyButton, true)
            RemoveButtonFromEscStack(HotkeyExit)
            selectingKey = false
            frameSelected = -1
            BlzFrameSetText(HotkeyMessage, "")
            edits = {}
        end
    end

    local function HotkeySaveFunc()
        if GetTriggerPlayer() == LocalPlayer then
            for key, list in pairs(edits) do
                for meta, id in pairs(list) do
                    local pair = referencePair[id]
                    if pair then
                        frameWithKey[pair[1]][pair[2]] = nil
                    end
                    referencePair[id] = {key, meta}
                end
                frameWithKey[key] = list
            end
            edits = {}
            BlzFrameSetText(HotkeyMessage, "|cff00FF00Hotkeys saved|r")
        end
        SaveHotkeys(GetTriggerPlayer())
    end

    local function ShowMenu()
        if GetTriggerPlayer() == LocalPlayer then
            BlzFrameSetVisible(HotkeyMenu, true)
            BlzFrameSetEnable(HotkeyButton, false)
            AddButtonToEscStack(HotkeyExit)
            UpdateHotkeys()
        end
    end

    local function InitFrames()
        local t = nil ---@type trigger

        HotkeyButton = BlzCreateFrame("IconButtonTemplate", BlzGetFrameByName("ConsoleUIBackdrop", 0), 0, 0)
        BlzFrameSetAbsPoint(HotkeyButton, FRAMEPOINT_TOPLEFT, 0.475000, 0.180000)
        BlzFrameSetAbsPoint(HotkeyButton, FRAMEPOINT_BOTTOMRIGHT, 0.510000, 0.145000)
        t = CreateTrigger()
        BlzTriggerRegisterFrameEvent(t, HotkeyButton, FRAMEEVENT_CONTROL_CLICK)
        TriggerAddAction(t, ShowMenu)
        BlzFrameSetVisible(HotkeyButton, false)
        AddFrameToMenu(HotkeyButton)
        AddDefaultTooltip(HotkeyButton, "Hotkeys", "Edit the hotkeys of the UI.")

        BackdropHotkeyButton = BlzCreateFrameByType("BACKDROP", "BackdropHotkeyButton", HotkeyButton, "", 0)
        BlzFrameSetAllPoints(BackdropHotkeyButton, HotkeyButton)
        BlzFrameSetTexture(BackdropHotkeyButton, "ReplaceableTextures\\CommandButtons\\BTNKeyboardIcon.blp", 0, true)

        HotkeyMenu = BlzCreateFrame("EscMenuBackdrop", BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), 0, 0)
        BlzFrameSetAbsPoint(HotkeyMenu, FRAMEPOINT_TOPLEFT, 0.140000, 0.530000)
        BlzFrameSetAbsPoint(HotkeyMenu, FRAMEPOINT_BOTTOMRIGHT, 0.660000, 0.190000)
        BlzFrameSetVisible(HotkeyMenu, false)

        HotkeyMessage = BlzCreateFrameByType("TEXT", "name", HotkeyMenu, "", 0)
        BlzFrameSetScale(HotkeyMessage, 1.00)
        BlzFrameSetPoint(HotkeyMessage, FRAMEPOINT_TOPLEFT, HotkeyMenu, FRAMEPOINT_TOPLEFT, 0.23000, -0.020000)
        BlzFrameSetPoint(HotkeyMessage, FRAMEPOINT_BOTTOMRIGHT, HotkeyMenu, FRAMEPOINT_BOTTOMRIGHT, -0.040000, 0.29000)
        BlzFrameSetText(HotkeyMessage, "")
        BlzFrameSetTextAlignment(HotkeyMessage, TEXT_JUSTIFY_TOP, TEXT_JUSTIFY_LEFT)

        -- Backpack

        HotkeyBackpack = BlzCreateFrame("ScriptDialogButton", HotkeyMenu, 0, 0)
        BlzFrameSetPoint(HotkeyBackpack, FRAMEPOINT_TOPLEFT, HotkeyMenu, FRAMEPOINT_TOPLEFT, 0.050000, -0.080000)
        BlzFrameSetPoint(HotkeyBackpack, FRAMEPOINT_BOTTOMRIGHT, HotkeyMenu, FRAMEPOINT_BOTTOMRIGHT, -0.30000, 0.22000)
        BlzFrameSetText(HotkeyBackpack, "|cffFCD20DBackpack|r")
        t = CreateTrigger()
        BlzTriggerRegisterFrameEvent(t, HotkeyBackpack, FRAMEEVENT_CONTROL_CLICK)
        TriggerAddAction(t, HotkeyBackpackFunc)

        HotkeyBackpackSubMenu = BlzCreateFrameByType("BACKDROP", "BACKDROP", HotkeyMenu, "", 1)
        BlzFrameSetPoint(HotkeyBackpackSubMenu, FRAMEPOINT_TOPLEFT, HotkeyMenu, FRAMEPOINT_TOPLEFT, 0.23000, -0.030000)
        BlzFrameSetPoint(HotkeyBackpackSubMenu, FRAMEPOINT_BOTTOMRIGHT, HotkeyMenu, FRAMEPOINT_BOTTOMRIGHT, -0.030000, 0.050000)
        BlzFrameSetTexture(HotkeyBackpackSubMenu, "war3mapImported\\EmptyBTN.blp", 0, true)
        BlzFrameSetVisible(HotkeyBackpackSubMenu, false)

        local HotkeyBackpackSubMenuBackdrop = BlzCreateFrame("QuestButtonBaseTemplate", HotkeyBackpackSubMenu, 0, 0)
        BlzFrameSetPoint(HotkeyBackpackSubMenuBackdrop, FRAMEPOINT_TOPLEFT, HotkeyBackpackSubMenu, FRAMEPOINT_TOPLEFT, 0.11000, -0.020000)
        BlzFrameSetPoint(HotkeyBackpackSubMenuBackdrop, FRAMEPOINT_BOTTOMRIGHT, HotkeyBackpackSubMenu, FRAMEPOINT_BOTTOMRIGHT, -0.010000, 0.050000)

        local HotkeyBackpackSubMenuButton = BlzCreateFrame("IconButtonTemplate", HotkeyBackpackSubMenu, 0, 0)
        BlzFrameSetPoint(HotkeyBackpackSubMenuButton, FRAMEPOINT_TOPLEFT, HotkeyBackpackSubMenu, FRAMEPOINT_TOPLEFT, 0.030000, -0.10000)
        BlzFrameSetPoint(HotkeyBackpackSubMenuButton, FRAMEPOINT_BOTTOMRIGHT, HotkeyBackpackSubMenu, FRAMEPOINT_BOTTOMRIGHT, -0.18000, 0.11000)
        AsingHotkey(HotkeyBackpackSubMenuButton, 0)

        local BackdropHotkeyBackpackSubMenuButton = BlzCreateFrameByType("BACKDROP", "BackdropHotkeyBackpackSubMenuButton", HotkeyBackpackSubMenuButton, "", 0)
        BlzFrameSetAllPoints(BackdropHotkeyBackpackSubMenuButton, HotkeyBackpackSubMenuButton)
        BlzFrameSetTexture(BackdropHotkeyBackpackSubMenuButton, "ReplaceableTextures\\CommandButtons\\BTNBackpackIcon.blp", 0, true)

        local HotkeyBackpackSubMenuText = BlzCreateFrameByType("TEXT", "name", HotkeyBackpackSubMenuBackdrop, "", 0)
        BlzFrameSetScale(HotkeyBackpackSubMenuText, 1.00)
        BlzFrameSetPoint(HotkeyBackpackSubMenuText, FRAMEPOINT_TOPLEFT, HotkeyBackpackSubMenuBackdrop, FRAMEPOINT_TOPLEFT, 0.010000, -0.0050000)
        BlzFrameSetPoint(HotkeyBackpackSubMenuText, FRAMEPOINT_BOTTOMRIGHT, HotkeyBackpackSubMenuBackdrop, FRAMEPOINT_BOTTOMRIGHT, -0.010000, 0.12000)
        BlzFrameSetText(HotkeyBackpackSubMenuText, "Use an item for the focused digimon")
        BlzFrameSetEnable(HotkeyBackpackSubMenuText, false)
        BlzFrameSetTextAlignment(HotkeyBackpackSubMenuText, TEXT_JUSTIFY_TOP, TEXT_JUSTIFY_LEFT)

        local HotkeyBackpackSubMenuDiscard = BlzCreateFrame("ScriptDialogButton", HotkeyBackpackSubMenuBackdrop, 0, 0)
        BlzFrameSetPoint(HotkeyBackpackSubMenuDiscard, FRAMEPOINT_TOPLEFT, HotkeyBackpackSubMenuBackdrop, FRAMEPOINT_TOPLEFT, 0.090000, -0.19245)
        BlzFrameSetPoint(HotkeyBackpackSubMenuDiscard, FRAMEPOINT_BOTTOMRIGHT, HotkeyBackpackSubMenuBackdrop, FRAMEPOINT_BOTTOMRIGHT, -0.010000, 0.0025500)
        BlzFrameSetText(HotkeyBackpackSubMenuDiscard, "|cffFCD20DDiscard|r")
        BlzFrameSetScale(HotkeyBackpackSubMenuDiscard, 0.858)

        AsingHotkey(HotkeyBackpackSubMenuDiscard, 1)

        local HotkeyBackpackSubMenuDrop = BlzCreateFrame("ScriptDialogButton", HotkeyBackpackSubMenuBackdrop, 0, 0)
        BlzFrameSetPoint(HotkeyBackpackSubMenuDrop, FRAMEPOINT_TOPLEFT, HotkeyBackpackSubMenuBackdrop, FRAMEPOINT_TOPLEFT, 0.010000, -0.19245)
        BlzFrameSetPoint(HotkeyBackpackSubMenuDrop, FRAMEPOINT_BOTTOMRIGHT, HotkeyBackpackSubMenuBackdrop, FRAMEPOINT_BOTTOMRIGHT, -0.090000, 0.0025500)
        BlzFrameSetText(HotkeyBackpackSubMenuDrop, "|cffFCD20DDrop|r")
        BlzFrameSetScale(HotkeyBackpackSubMenuDrop, 0.858)

        AsingHotkey(HotkeyBackpackSubMenuDrop, 2)

        local x, y = {}, {}
        local stepSize = 0.03

        local startY = -0.030000
        for row = 1, 4 do
            local startX = 0.010000
            for colum = 1, 4 do
                local index = 4 * (row - 1) + colum

                x[index] = startX
                y[index] = startY

                startX = startX + stepSize
            end
            startY = startY - stepSize
        end

        local indexes = {3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}
        for i = 1, udg_MAX_ITEMS do
            local HotkeyBackpackSubMenuItem = BlzCreateFrame("IconButtonTemplate", HotkeyBackpackSubMenuBackdrop, 0, 0)
            BlzFrameSetPoint(HotkeyBackpackSubMenuItem, FRAMEPOINT_TOPLEFT, HotkeyBackpackSubMenuBackdrop, FRAMEPOINT_TOPLEFT, x[i], y[i])
            BlzFrameSetSize(HotkeyBackpackSubMenuItem, stepSize, stepSize)

            local BackdropHotkeyBackpackSubMenuItem = BlzCreateFrameByType("BACKDROP", "BackdropHotkeyBackpackSubMenuItem[" .. i .. "]", HotkeyBackpackSubMenuItem, "", 0)
            BlzFrameSetAllPoints(BackdropHotkeyBackpackSubMenuItem, HotkeyBackpackSubMenuItem)
            BlzFrameSetTexture(BackdropHotkeyBackpackSubMenuItem, "ReplaceableTextures\\CommandButtons\\BTNCancel.blp", 0, true)
            AsingHotkey(HotkeyBackpackSubMenuItem, indexes[i]) -- start in 3 and end in 19
        end

        -- Your digimons

        local x1 = {}
        local y1 = {}
        local x2 = {}
        local y2 = {}

        for i = 0, 3 do
            for j = 0, 1 do
                local index = i + 4 * j
                x1[index] = 0.022500 + i * 0.045
                y1[index] = -0.05 - j * 0.045
                x2[index] = -0.022500 - (3 - i) * 0.045
                y2[index] = 0.05 + (1 - j) * 0.045
            end
        end

        HotkeyYourDigimons = BlzCreateFrame("ScriptDialogButton", HotkeyMenu, 0, 0)
        BlzFrameSetPoint(HotkeyYourDigimons, FRAMEPOINT_TOPLEFT, HotkeyMenu, FRAMEPOINT_TOPLEFT, 0.050000, -0.040000)
        BlzFrameSetPoint(HotkeyYourDigimons, FRAMEPOINT_BOTTOMRIGHT, HotkeyMenu, FRAMEPOINT_BOTTOMRIGHT, -0.30000, 0.26000)
        BlzFrameSetText(HotkeyYourDigimons, "|cffFCD20DYour digimons|r")
        t = CreateTrigger()
        BlzTriggerRegisterFrameEvent(t, HotkeyYourDigimons, FRAMEEVENT_CONTROL_CLICK)
        TriggerAddAction(t, HotkeyYourDigimonsFunc)

        HotkeyYourDigimonsSubMenu = BlzCreateFrameByType("BACKDROP", "BACKDROP", HotkeyMenu, "", 1)
        BlzFrameSetPoint(HotkeyYourDigimonsSubMenu, FRAMEPOINT_TOPLEFT, HotkeyMenu, FRAMEPOINT_TOPLEFT, 0.23000, -0.030000)
        BlzFrameSetPoint(HotkeyYourDigimonsSubMenu, FRAMEPOINT_BOTTOMRIGHT, HotkeyMenu, FRAMEPOINT_BOTTOMRIGHT, -0.030000, 0.050000)
        BlzFrameSetTexture(HotkeyYourDigimonsSubMenu, "war3mapImported\\EmptyBTN.blp", 0, true)
        BlzFrameSetVisible(HotkeyYourDigimonsSubMenu, false)

        local HotkeyYourDigimonsSubMenuBackdrop = BlzCreateFrame("EscMenuBackdrop", HotkeyYourDigimonsSubMenu, 0, 0)
        BlzFrameSetPoint(HotkeyYourDigimonsSubMenuBackdrop, FRAMEPOINT_TOPLEFT, HotkeyYourDigimonsSubMenu, FRAMEPOINT_TOPLEFT, 0.060000, -0.025000)
        BlzFrameSetPoint(HotkeyYourDigimonsSubMenuBackdrop, FRAMEPOINT_BOTTOMRIGHT, HotkeyYourDigimonsSubMenu, FRAMEPOINT_BOTTOMRIGHT, 0.020000, 0.045000)

        local HotkeyYourDigimonsSubMenuButton = BlzCreateFrame("IconButtonTemplate", HotkeyYourDigimonsSubMenu, 0, 0)
        BlzFrameSetPoint(HotkeyYourDigimonsSubMenuButton, FRAMEPOINT_TOPLEFT, HotkeyYourDigimonsSubMenu, FRAMEPOINT_TOPLEFT, 0.0050000, -0.10000)
        BlzFrameSetPoint(HotkeyYourDigimonsSubMenuButton, FRAMEPOINT_BOTTOMRIGHT, HotkeyYourDigimonsSubMenu, FRAMEPOINT_BOTTOMRIGHT, -0.20500, 0.11000)
        AsingHotkey(HotkeyYourDigimonsSubMenuButton, 20)

        local BackdropHotkeyYourDigimonsSubMenuButton = BlzCreateFrameByType("BACKDROP", "BackdropHotkeyYourDigimonsSubMenuButton", HotkeyYourDigimonsSubMenuButton, "", 0)
        BlzFrameSetAllPoints(BackdropHotkeyYourDigimonsSubMenuButton, HotkeyYourDigimonsSubMenuButton)
        BlzFrameSetTexture(BackdropHotkeyYourDigimonsSubMenuButton, "ReplaceableTextures\\CommandButtons\\BTNDigimonsIcon.blp", 0, true)

        indexes = {[0] = 21, 22, 23, 24, 25, 26, 27, 28}

        for i = 0, 7 do
            local DigimonT = BlzCreateFrame("ScriptDialogButton", HotkeyYourDigimonsSubMenuBackdrop, 0, 0)
            BlzFrameSetPoint(DigimonT, FRAMEPOINT_TOPLEFT, HotkeyYourDigimonsSubMenuBackdrop, FRAMEPOINT_TOPLEFT, x1[i], y1[i])
            BlzFrameSetPoint(DigimonT, FRAMEPOINT_BOTTOMRIGHT, HotkeyYourDigimonsSubMenuBackdrop, FRAMEPOINT_BOTTOMRIGHT, x2[i], y2[i])
            local BackdropDigimonT = BlzCreateFrameByType("BACKDROP", "HotkeyBackdropDigimonT[" .. i .. "]", DigimonT, "", 1)
            BlzFrameSetAllPoints(BackdropDigimonT, DigimonT)
            BlzFrameSetTexture(BackdropDigimonT, "ReplaceableTextures\\CommandButtons\\BTNCancel.blp", 0, true)
            BlzFrameSetLevel(BackdropDigimonT, 1)
            --BlzFrameSetScale(DigimonT, 0.8)
            AsingHotkey(DigimonT, indexes[i])
        end

        local Text = BlzCreateFrameByType("TEXT", "name", HotkeyYourDigimonsSubMenuBackdrop, "", 0)
        BlzFrameSetPoint(Text, FRAMEPOINT_TOPLEFT, HotkeyYourDigimonsSubMenuBackdrop, FRAMEPOINT_TOPLEFT, 0.050000, -0.020000)
        BlzFrameSetPoint(Text, FRAMEPOINT_BOTTOMRIGHT, HotkeyYourDigimonsSubMenuBackdrop, FRAMEPOINT_BOTTOMRIGHT, -0.050000, 0.14000)
        BlzFrameSetText(Text, "|cffFFCC00Choose a Digimon|r")
        BlzFrameSetEnable(Text, false)
        BlzFrameSetTextAlignment(Text, TEXT_JUSTIFY_CENTER, TEXT_JUSTIFY_MIDDLE)
        --BlzFrameSetScale(Text, 0.8)

        local Summon = BlzCreateFrame("ScriptDialogButton", HotkeyYourDigimonsSubMenuBackdrop,0,0)
        BlzFrameSetPoint(Summon, FRAMEPOINT_TOPLEFT, HotkeyYourDigimonsSubMenuBackdrop, FRAMEPOINT_TOPLEFT, 0.030000, -0.14500)
        BlzFrameSetPoint(Summon, FRAMEPOINT_BOTTOMRIGHT, HotkeyYourDigimonsSubMenuBackdrop, FRAMEPOINT_BOTTOMRIGHT, -0.11000, 0.02000)
        BlzFrameSetText(Summon, "|cffFCD20DSummon|r")
        --BlzFrameSetScale(Summon, 0.8)
        AsingHotkey(Summon, 29)

        local Store = BlzCreateFrame("ScriptDialogButton", HotkeyYourDigimonsSubMenuBackdrop,0,0)
        BlzFrameSetPoint(Store, FRAMEPOINT_TOPLEFT, HotkeyYourDigimonsSubMenuBackdrop, FRAMEPOINT_TOPLEFT, 0.030000, -0.1700)
        BlzFrameSetPoint(Store, FRAMEPOINT_BOTTOMRIGHT, HotkeyYourDigimonsSubMenuBackdrop, FRAMEPOINT_BOTTOMRIGHT, -0.11000, -0.00500)
        BlzFrameSetText(Store, "|cffFCD20DStore|r")
        --BlzFrameSetScale(Store, 0.8)
        AsingHotkey(Store, 30)

        local Free = BlzCreateFrame("ScriptDialogButton", HotkeyYourDigimonsSubMenuBackdrop,0,0)
        BlzFrameSetPoint(Free, FRAMEPOINT_TOPLEFT, HotkeyYourDigimonsSubMenuBackdrop, FRAMEPOINT_TOPLEFT, 0.11000, -0.14500)
        BlzFrameSetPoint(Free, FRAMEPOINT_BOTTOMRIGHT, HotkeyYourDigimonsSubMenuBackdrop, FRAMEPOINT_BOTTOMRIGHT, -0.030000, 0.02000)
        BlzFrameSetText(Free, "|cffFCD20DFree|r")
        AsingHotkey(Free, 31)

        local Warning = BlzCreateFrame("QuestButtonBaseTemplate", Free,0,0)
        BlzFrameSetPoint(Warning, FRAMEPOINT_TOPLEFT, Free, FRAMEPOINT_TOPLEFT, -0.020000, -0.025000)
        BlzFrameSetPoint(Warning, FRAMEPOINT_BOTTOMRIGHT, Free, FRAMEPOINT_BOTTOMRIGHT, 0.030000, -0.060000)

        local AreYouSure = BlzCreateFrameByType("TEXT", "name", Warning, "", 0)
        BlzFrameSetPoint(AreYouSure, FRAMEPOINT_TOPLEFT, Warning, FRAMEPOINT_TOPLEFT, 0.0050000, -0.0050000)
        BlzFrameSetPoint(AreYouSure, FRAMEPOINT_BOTTOMRIGHT, Warning, FRAMEPOINT_BOTTOMRIGHT, -0.0050000, 0.025000)
        BlzFrameSetText(AreYouSure, "|cffFFCC00Are you sure you wanna free this digimon?|r")
        --BlzFrameSetScale(AreYouSure, 0.8)
        BlzFrameSetTextAlignment(AreYouSure, TEXT_JUSTIFY_CENTER, TEXT_JUSTIFY_MIDDLE)

        local Yes = BlzCreateFrame("ScriptDialogButton", Warning,0,0)
        BlzFrameSetPoint(Yes, FRAMEPOINT_TOPLEFT, Warning, FRAMEPOINT_TOPLEFT, 0.010000, -0.035000)
        BlzFrameSetPoint(Yes, FRAMEPOINT_BOTTOMRIGHT, Warning, FRAMEPOINT_BOTTOMRIGHT, -0.070000, 0.0050000)
        BlzFrameSetText(Yes, "|cffFCD20DYes|r")
        --BlzFrameSetScale(Yes, 0.8)
        AsingHotkey(Yes, 32)

        local No = BlzCreateFrame("ScriptDialogButton", Warning,0,0)
        BlzFrameSetPoint(No, FRAMEPOINT_TOPLEFT, Warning, FRAMEPOINT_TOPLEFT, 0.070000, -0.035000)
        BlzFrameSetPoint(No, FRAMEPOINT_BOTTOMRIGHT, Warning, FRAMEPOINT_BOTTOMRIGHT, -0.010000, 0.0050000)
        BlzFrameSetText(No, "|cffFCD20DNo|r")
        --BlzFrameSetScale(No, 0.8)
        AsingHotkey(No, 33)

        --BlzFrameSetScale(Warning, 0.8)

        --BlzFrameSetScale(Free, 0.8)

        --BlzFrameSetScale(HotkeyYourDigimonsSubMenuBackdrop, 0.8)


        HotkeyExit = BlzCreateFrame("ScriptDialogButton", HotkeyMenu, 0, 0)
        BlzFrameSetScale(HotkeyExit, 1.00)
        BlzFrameSetPoint(HotkeyExit, FRAMEPOINT_TOPLEFT, HotkeyMenu, FRAMEPOINT_TOPLEFT, 0.34000, -0.30000)
        BlzFrameSetPoint(HotkeyExit, FRAMEPOINT_BOTTOMRIGHT, HotkeyMenu, FRAMEPOINT_BOTTOMRIGHT, -0.10000, 0.010000)
        BlzFrameSetText(HotkeyExit, "|cffFCD20DExit|r")
        t = CreateTrigger()
        BlzTriggerRegisterFrameEvent(t, HotkeyExit, FRAMEEVENT_CONTROL_CLICK)
        TriggerAddAction(t, HotkeyExitFunc)

        HotkeySave = BlzCreateFrame("ScriptDialogButton", HotkeyMenu, 0, 0)
        BlzFrameSetScale(HotkeySave, 1.00)
        BlzFrameSetPoint(HotkeySave, FRAMEPOINT_TOPLEFT, HotkeyMenu, FRAMEPOINT_TOPLEFT, 0.10000, -0.30000)
        BlzFrameSetPoint(HotkeySave, FRAMEPOINT_BOTTOMRIGHT, HotkeyMenu, FRAMEPOINT_BOTTOMRIGHT, -0.34000, 0.010000)
        BlzFrameSetText(HotkeySave, "|cffFCD20DSave|r")
        t = CreateTrigger()
        BlzTriggerRegisterFrameEvent(t, HotkeySave, FRAMEEVENT_CONTROL_CLICK)
        TriggerAddAction(t, HotkeySaveFunc)
    end

    FrameLoaderAdd(InitFrames)

    OnInit.final(function ()
        local t = CreateTrigger()
        for k, v in pairs(_G) do
            if Debug.wc3Type(v) == "oskeytype" and not IsBannedKey(v) then
                ForForce(FORCE_PLAYING, function ()
                    for meta = 0, 15 do
                        BlzTriggerRegisterPlayerKeyEvent(t, GetEnumPlayer(), v, meta, true)
                    end
                end)
                oskeyName[v] = string.sub(k, 7)
                oskeyConverted[GetHandleId(v)] = v
            end
        end
        TriggerAddAction(t, function ()
            if GetTriggerPlayer() == LocalPlayer then
                local key = BlzGetTriggerPlayerKey()
                local meta = BlzGetTriggerPlayerMetaKey()
                if selectingKey then
                    local frame = frames[frameSelected]
                    if frame then
                        selectingKey = false
                        if not edits[key] then
                            edits[key] = {}
                        end

                        local index = edits[key][meta] or (frameWithKey[key] and frameWithKey[key][meta])
                        if index then
                            BlzFrameSetText(hotkeyText[index], "")
                        end

                        edits[key][meta] = frameSelected

                        BlzFrameSetText(hotkeyText[frameSelected], ((meta ~= 0) and (OskeyMeta[meta] .. " + ") or "") .. oskeyName[key])
                        BlzFrameSetText(HotkeyMessage, "")
                    end
                    frameSelected = -1
                else
                    local id = frameWithKey[key] and frameWithKey[key][meta]
                    if id and frames[id] and BlzFrameIsVisible(frames[id]) and BlzFrameGetEnable(frames[id]) then
                        BlzFrameClick(frames[id])
                    end
                end
            end
        end)

        ForForce(FORCE_PLAYING, function ()
            LoadHotkeys(GetEnumPlayer())
        end)
    end)

    ---@param frame framehandle
    ---@param id integer
    function AssignFrame(frame, id)
        frames[id] = frame
        referenceFrame[frame] = id
    end

    ---@param p player
    ---@param flag boolean
    function ShowHotkeys(p, flag)
        if p == LocalPlayer then
            BlzFrameSetVisible(HotkeyButton, flag)
        end
    end

    ---@param p player
    function SaveHotkeys(p)
        local savecode = Savecode.create()
        local length1 = 0
        for key, list in pairs(frameWithKey) do
            local length2 = 0
            for meta, id in pairs(list) do
                savecode:Encode(id, MAX_KEYS) -- save the id
                savecode:Encode(meta, 15) -- save the metakey
                length2 = length2 + 1
            end
            savecode:Encode(length2, 16) -- save the length of the metakey list
            savecode:Encode(GetHandleId(key), MAX_KEYS) -- save the key
            length1 = length1 + 1
        end
        savecode:Encode(length1, MAX_KEYS) -- save the length of the key list
        local save = savecode:Save(LocalPlayer, 1)

        if p == LocalPlayer then
            FileIO.Write(SaveFile.getFolder() .. "\\Hotkeys.pld", save)
        end
        savecode:destroy()
    end

    ---@param p player
    function LoadHotkeys(p)
        local loaded = GetSyncedData(p, FileIO.Read, SaveFile.getFolder() .. "\\Hotkeys.pld")
        if loaded:len() > 1 then
            local savecode = Savecode.create()
            if savecode:Load(p, loaded, 1) then
                local length1 = savecode:Decode(MAX_KEYS) -- load the length of the key list
                for _ = 1, length1 do
                    local key = oskeyConverted[savecode:Decode(MAX_KEYS)] -- load the key
                    if p == LocalPlayer then
                        frameWithKey[key] = {}
                    end
                    local length2 = savecode:Decode(16) -- load the length of the metakey list
                    for _ = 1, length2 do
                        local meta = savecode:Decode(15) -- load the metakey
                        local id  = savecode:Decode(MAX_KEYS) -- load the id
                        if p == LocalPlayer then
                            frameWithKey[key][meta] = id
                            referencePair[id] = {key, meta}
                        end
                    end
                end
                if p == LocalPlayer then
                    UpdateHotkeys()
                end
                DisplayTextToPlayer(p, 0, 0, "Hotkeys loaded")
            end
            savecode:destroy()
        end
    end
end)
Debug.endFile()
In case of needing saving values that affect the outcome of the game, I just use a combination of both.
 
Last edited:
Level 24
Joined
Jun 26, 2020
Messages
1,850
I'm having a weird issue with my system, look in this part if you understood the system:
Lua:
coroutine.resume(actThread, pcall(Str2Obj, actString))

if areWaiting[actThread] then
    for _, thr in ipairs(areWaiting[actThread]) do
        coroutine.resume(thr)
    end
    areWaiting[actThread] = nil
end
In the first part I'm resuming the thread stopped by the sync and the second part I'm resumming the threads yielded by WaitLastSync() that waited the sync I mentioned, so looking at this order the actThread should end running before the areWaiting threads, but for some reason I'm getting the case were the areWaiting threads are resumed before the actThread, and I'm getting troubles for that, what do you think is happening?
 
Top