Lua: Adding custom user configurable hotkeys
2 2

10 posts in this topic

Recommended Posts

Posted (edited)

So... you want hotkeys the player can press that does something for your mod.

 

The problem:

The player should be able to reconfigure these keys, so they don't conflict with the players existing setup or other mods they have loaded.

Sure you can have these keys setup in a 'settings.lua' file the player can edit, but that leads to its own problems...

If a user edits this file, and the mod is designed for multiplayer, then they'll have issues connecting to servers since the checksums won't match. Maybe you're feeling clever, and decided to write these settings into a settings.ini file instead, and just read the ini on loading (thus not having to worry about checksums), but that's a problem too....users rarely read the documentation, and hate manually editing files in notepad!

So...the only real answer is to have a proper GUI where they can edit these settings, and what better GUI to use then PZ's own options screen!

 

For this tutorial I'll explain using some work I've been doing on the ORGM mod today. I needed 3 new hotkeys: for equipping the best pistol, best rifle, and one for the best shotgun.

1.thumb.jpg.7c4aa66887db8ed55b23d395ad07f91a.jpg

 

Nice and clean there, no need to manually edit file, or screw up the luachecksums, or need to explain to users how to do it other then: 'it will be in your pz options screen'

 

Here's our script in full:

Spoiler

--' We need to use the global keyBinding table, this stores all our binding values'
local index = nil -- index will be the position we want to insert into the table
for i,b in ipairs(keyBinding) do
    --' we need to find the index of the item we want to insert after
    -- in this case its "Equip/Unequip Stab weapon"'
    if b.value == "Equip/Unequip Stab weapon" then
        index = i --' found the index, set it and break from the loop'
        break
    end
end

if index then
    --' we got a index, first lets insert our new entries'
    table.insert(keyBinding, index+1, {value = "Equip/Unequip Pistol", key = 5})
    table.insert(keyBinding, index+2, {value = "Equip/Unequip Rifle", key = 6})
    table.insert(keyBinding, index+3, {value = "Equip/Unequip Shotgun", key = 7})
    
    --' store the original MainOptions:create() method in a variable'
    local oldCreate = MainOptions.create

    --' overwrite it'
    function MainOptions:create()
        oldCreate(self)
        for _, keyTextElement in pairs(MainOptions.keyText) do repeat
            --' if keyTextElement is nil or doesnt have a ISLabel, break out of the 
            -- "repeat ... until true"  loop, and continue with the "for .. do ... end" 
            -- loop'
            if not keyTextElement or not keyTextElement.txt then break end
            
            local label = keyTextElement.txt --' our ISLabel item is stored in keyTextElement.txt
            -- We need to do a few things here to prep the new entries.
            -- 1) We wont have a proper translation, and the translation will be set to
            --    "UI_optionscreen_binding_Equip/Unequip Pistol", which will look funny on the 
            --    options screen, so we need to fix
            -- 2) the new translation doesnt properly adjust the x position and width, so we need to 
            --    manually adjust these'
            
            if label.name == "Equip/Unequip Pistol" then
                label:setTranslation("Equip/Unequip Pistol")
                label:setX(label.x)
                label:setWidth(label.width)
            elseif label.name == "Equip/Unequip Rifle" then 
                label:setTranslation("Equip/Unequip Rifle")
                label:setX(label.x)
                label:setWidth(label.width)
            elseif label.name == "Equip/Unequip Shotgun" then 
                label:setTranslation("Equip/Unequip Shotgun")
                label:setX(label.x)
                label:setWidth(label.width)
            end
        until true end
    end
end

--' now add the event hook function, for this example the player just says the name of the key'
Events.OnKeyPressed.Add(function(key)
    local player = getSpecificPlayer(0)
    if key == getCore():getKey("Equip/Unequip Pistol") then
        player:Say("Key pressed: Equip/Unequip Pistol")
    elseif key == getCore():getKey("Equip/Unequip Rifle") then
        player:Say("Key pressed: Equip/Unequip Rifle")
    elseif key == getCore():getKey("Equip/Unequip Shotgun") then
        player:Say("Key pressed: Equip/Unequip Shotgun")
    end
end)

 

 

All the key bindings listed on the options screen are stored in the global table called keyBinding

First we need to find the index of the option we want to insert after:

local index = nil
for i,b in ipairs(keyBinding) do
    if b.value == "Equip/Unequip Stab weapon" then
        index = i
        break
    end
end

The next chunks of code are wrapping in a 'if index then .... end' block, but I'll omit that here just so I can break this 'if statement' into smaller pieces (you can see it in the full script in the spoiler above)

 

Since we got our index and know our insertion point, lets insert the new options:

    table.insert(keyBinding, index+1, {value = "Equip/Unequip Pistol", key = 5})
    table.insert(keyBinding, index+2, {value = "Equip/Unequip Rifle", key = 6})
    table.insert(keyBinding, index+3, {value = "Equip/Unequip Shotgun", key = 7})

Note these keys actually correspond to 4, 5 and 6 respectively (esc is key=1, swinging weapon is key=2, firearm is key=3, and stabbing weapon is key=4)

You can also use constant values for these, if you look in shared/keyBinding.lua, you'll see rack firearm is key = Keyboard.KEY_X

 

Now we need to overwrite the MainOptions:create() method, so we can fix some translation issues:

    local oldCreate = MainOptions.create

    function MainOptions:create()
        oldCreate(self)
        for _, keyTextElement in pairs(MainOptions.keyText) do repeat
            if not keyTextElement or not keyTextElement.txt then break end

            local label = keyTextElement.txt
            if label.name == "Equip/Unequip Pistol" then
                label:setTranslation("Equip/Unequip Pistol")
                label:setX(label.x)
                label:setWidth(label.width)
            elseif label.name == "Equip/Unequip Rifle" then 
                label:setTranslation("Equip/Unequip Rifle")
                label:setX(label.x)
                label:setWidth(label.width)
            elseif label.name == "Equip/Unequip Shotgun" then 
                label:setTranslation("Equip/Unequip Shotgun")
                label:setX(label.x)
                label:setWidth(label.width)
            end
        until true end
    end

Notice the first thing in that block is we store the original MainOptions:create() method in a variable, and call it first thing in our overwrite.  By doing this we're just basically appending our new code to the end of the original function.

The problem is if we don't do this, our options will appear as "UI_optionscreen_binding_Equip/Unequip Pistol" on the screen, unless you have a proper translation entry. Since we dont, we need to change the label translations so we don't have that "UI_optionscreen_binding_" prefix.

As well as changing the text that gets shown, we need to reset the x position, and the width of the label or they'll still be positioned too far off. By calling label:setTranslation() it fixes the label.x and label.width variables, but doesn't actually adjust the positions so we need to call label:setX() and label:setWidth() manually.

 

And thats it for the options screen...all you need to do to have the custom keys show up and be configurable. This is where our 'if index then .... end' block actually ends.

Note you'll still have the translation problem when the user clicks to change the key to something else, the popup will need fixing.... you have to overwrite MainOptions:render() for that, I won't be covering that here since its a minor issue, and doing that might be incompatible if 2 mods use this same technique. The code above can be used by multiple mods at the same time without issue, even with our overwrite:

If your mod overwrites MainOptions:create(), and another mod loads after and uses the same technique, then their overwrite ends up calling yours, and yours then calls the original function.

 

Now for the final piece, the event hook when the player presses a key:

Events.OnKeyPressed.Add(function(key)
    local player = getSpecificPlayer(0)
    if key == getCore():getKey("Equip/Unequip Pistol") then
        player:Say("Key pressed: Equip/Unequip Pistol")
    elseif key == getCore():getKey("Equip/Unequip Rifle") then
        player:Say("Key pressed: Equip/Unequip Rifle")
    elseif key == getCore():getKey("Equip/Unequip Shotgun") then
        player:Say("Key pressed: Equip/Unequip Shotgun")
    end
end)

Our key presses here don't actually do much, the player just says which of the keys options got triggered.

 

But that's all there is to it, custom user configurable keys that don't force players to manually edit files, and bypass server lua checksum comparisons. ;)

 

 

Edited by Fenris_Wolf

Share this post


Link to post
Share on other sites
8 hours ago, Hideki-Ishimura said:

Hello I would like to start creating a simple mod in Lua, but I don't know what IDE to use, what is the IDE used by the community? Thanks!

I'd imagine the most used is probably just notepad++ which is what I use, though that's not really a IDE.

A real IDE is probably overkill for PZ modding, plus a lot would choke since most of the actual functions, classes and methods are in the java components. If you really want a IDE the best one for lua is probably ZeroBrane, which seems to handle PZ mod projects fairly well, though It does flag a lot of PZ's global variables and functions as possible typos since it can't look them up.

Share this post


Link to post
Share on other sites

I use Atom to code for PZ (and as a general text editor). Prefer it over Notepad++ but needs some love at first to run like you want it. There are a bunch of plugins for it that will customize the editor to your liking. Mainly switched to it because it is available for other operating systems as well. 

Share this post


Link to post
Share on other sites
3 minutes ago, Dr_Cox1911 said:

Mainly switched to it because it is available for other operating systems as well. 

I haven't tried that one (I think, hard to remember I've burned through a lot of editors). Notepad++'s 'windows only' is a real downside, on the upside, it does run flawlessly using wine, which is what I use when I need a general editor on linux systems.

Share this post


Link to post
Share on other sites
Posted (edited)

Thanks for the example! Everything worked out.
But there is one problem.
What about the Tooltip?
It is necessary that in Tooltip_.txt
- except for the text the value was displayed
getCore():getKey(getText("Text"))

how to do it?

Edited by Nebula

Share this post


Link to post
Share on other sites
7 minutes ago, Nebula said:

What about the Tooltip?

Not sure what you mean..tooltips aren't really used on the keybinding screen.

The Tooltip_*.txt translation files are meant to used in game, when hovering the mouse over a item.

Share this post


Link to post
Share on other sites

Yes.
I need to have a designated key in the tooltip, besides the usual text.
So, as a TXT file, then it does not work in it to use variables from LUA how to be?

Share this post


Link to post
Share on other sites

And further...
I have a button on the destination screen

UI_optionscreen_binding_
As you warned, although I used the translation files ...
Here is the code.

 

local index = nil
for i,b in ipairs(keyBinding) do


    if b.value == "Toggle UI" then
        index = i 
        break
    end
end

if index then

    table.insert(keyBinding, index+1, {value = getText("IGUI_Show_window_of_equipped_items"), key = 24})

	local oldCreate = MainOptions.create

    function MainOptions:create()
        oldCreate(self)
        for _, keyTextElement in pairs(MainOptions.keyText) do repeat
            
            if not keyTextElement or not keyTextElement.txt then break end
            
            local label = keyTextElement.txt
                        
            if label.name == getText("IGUI_Show_window_of_equipped_items") then
                label:setTranslation(getText("IGUI_Show_window_of_equipped_items"))
                label:setX(label.x)
                label:setWidth(label.width)
            end
        until true end
    end
end

 

Share this post


Link to post
Share on other sites

If your going to use translation files, then most of that code becomes redundant. This tutorial is mostly outdated if your using translations.

The current version I'm using is:

-- Setup hotkey bindings
-- We need to use the global keyBinding table, this stores all our binding values
local index = nil -- index will be the position we want to insert into the table
for i,b in ipairs(keyBinding) do
    if b.value == "Equip/Unequip Stab weapon" then
        index = i -- found the index, set it and break from the loop
        break
    end
end

if index then
    -- we got a index, first lets insert our new entries
    table.insert(keyBinding, index+1, {value = "Equip/Unequip Pistol", key = 5})
    table.insert(keyBinding, index+2, {value = "Equip/Unequip Rifle", key = 6})
    table.insert(keyBinding, index+3, {value = "Equip/Unequip Shotgun", key = 7})
    table.insert(keyBinding, index+4, {value = "Reload Any Magazine", key = Keyboard.KEY_G })
    table.insert(keyBinding, index+5, {value = "Select Fire Toggle", key = Keyboard.KEY_Z })
    table.insert(keyBinding, index+6, {value = "Firearm Inspection Window", key = Keyboard.KEY_U })
end

Thats the Entire lua code, overwriting MainOptions:create() is not needed with translations.

Then in the UI_EN.txt I've added:

UI_EN = {
    UI_optionscreen_binding_Equip/Unequip Pistol = "Equip/Unequip Pistol",
    UI_optionscreen_binding_Equip/Unequip Rifle = "Equip/Unequip Rifle",
    UI_optionscreen_binding_Equip/Unequip Shotgun = "Equip/Unequip Shotgun",
    UI_optionscreen_binding_Reload Any Magazine = "Reload Any Magazine",
    UI_optionscreen_binding_Select Fire Toggle = "Select Fire Toggle",
    UI_optionscreen_binding_Firearm Inspection Window = "Firearm Inspection Window",
}

And thats all of it.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
2 2