armor.lua

by hugeblank
533 days agolua
COPY
1
-- Armorlock by hugeblank, March 2023
2
-- A simple and extensible armor swapping software.
3
-- - Configurable via the "armorlock" namespace:
4
--   - usage: `set armorlock.elytra_id sc_goodies:white_elytra`
5
--   - storage inventory target (storage) - Required
6
--   - manipulator target (manipulator) - Required
7
--   - player target(username) - Required
8
--   - elytra item target (elytra_id) - Optional, defaults to "minecraft:elytra"
9
--   - off-ground duration before elytra swap triggers (air_ticks) - Optional, defaults to 8
10
--   - held item slot to lock elytra swapping (held_slot) - Optional, defaults to 1
11
-- - Builtin handling for elytra swapping and gold boots:
12
--   - Elytra:
13
--     - Swaps ON if off-ground for a configurable amount of ticks (air_ticks).
14
--     - Swaps OFF on contact with the ground, UNLESS holding shift
15
--     - Will NOT swap if selecting the first inventory slot
16
--   - Boots:
17
--     - Swaps ON when in the nether
18
--     - Swaps OFF when not in the nether
19
-- - Easy to extend with the createslotHandler function:
20
--   - Easily creates a way of tracking and toggling armor swapping
21

22

23
-- Feel free to modify to your need, but don't expect/assume I'll help in any specific circumstance :thumbs_up:
24

25
assert(chatbox, "Chatbox not connected")
26

27
local config = {
28
    manipulator = settings.get("armorlock.manipulator"),
29
    storage = settings.get("armorlock.storage"),
30
    username = settings.get("armorlock.username"),
31
    air_ticks = tonumber(settings.get("armorlock.air_ticks")) or 8,
32
    held_slot = tonumber(settings.get("armorlock.held_slot")) or 1,
33
    elytra_id = settings.get("armorlock.elytra_id") or "minecraft:elytra",
34
}
35
-- Just a big old block of assertions, don't mind it.
36
assert(config.manipulator, "manipulator id missing, set with: `set armorlock.manipulator <peripheral id>`")
37
assert(config.storage, "storage id missing, set with: `set armorlock.storage <peripheral id>`")
38
assert(peripheral.isPresent(config.manipulator), "Could not find manipulator "..tostring(config.manipulator))
39
assert(peripheral.isPresent(config.storage), "Could not find storage "..tostring(config.storage))
40
assert(tonumber(config.air_ticks), "air ticks must be a number")
41
config.air_ticks = tonumber(config.air_ticks)
42
assert(config.air_ticks > 0, "air ticks must be greater than 0")
43
assert(tonumber(config.held_slot), "held slot must be a number")
44
config.held_slot = tonumber(config.held_slot)
45
assert(config.held_slot > 0 and config.held_slot < 10, "held slot must be between 1 and 9 inclusive")
46
if not config.elytra_id:find("elytra") then
47
    print("warn: armorlock.elytra_id does not contain elytra in the name, swapping may not function as intended")
48
end
49

50
local manipulator = peripheral.wrap(config.manipulator)
51
assert(manipulator.getMetaOwner, "Introspection Module & Entity sensor required for this program.")
52

53
local function swapItem(equipmentinv, condition, storageslot, equipmentslot)
54
    assert(storageslot%2 == 1, "Storage slot must be odd!")
55
    if condition then
56
        equipmentinv.pushItems(config.storage, equipmentslot, 1, storageslot)
57
        equipmentinv.pullItems(config.storage, storageslot+1, 1, equipmentslot)
58
    else
59
        equipmentinv.pushItems(config.storage, equipmentslot, 1, storageslot+1)
60
        equipmentinv.pullItems(config.storage, storageslot, 1, equipmentslot)
61
    end
62
end
63

64
-- Autoincrement value to control inventory spaces.
65
-- Make sure you're providing an inventory with enough spaces for the amount of slotHandlers you need!
66
-- floor(size/2) = max amount of slotHandlers.
67
local slotoffset = 1
68

69
-- Whether swapping is locked. Slot handlers should respect this value.
70
local lock = false
71

72
-- Handler functions
73
local handlers = {}
74

75
--- Creates a pair of functions to get and toggle the state of the armor in the players inventory
76
--- Passes these functions into the handler function when initially invoked
77
--- @param equipment function[] the equipment pseudo-peripheral provided by the manipulator
78
--- @param equipmentslot int the slot number of the equipment to be swapped. See the comment on the line that defines the equipment variable for the mapping.
79
--- @param name string the MC identifier of the item to be worn in the "active" state, where the condition is being met.
80
--- @param handler function The handler for this equipment slot
81
local function createSlotHandler(equipment, equipmentslot, name, handler)
82
    local storageslot = slotoffset
83
    slotoffset = slotoffset + 2
84
    local list = equipment.list()
85
    local condition = list[equipmentslot] and list[equipmentslot].name == name
86
    local getState, toggle = function() 
87
        return condition 
88
    end, function()
89
        swapItem(equipment, condition, storageslot, equipmentslot)
90
        condition = not condition
91
        return condition
92
    end
93
    handlers[#handlers+1] = {
94
        getState, 
95
        toggle,
96
        function()
97
            handler(getState, toggle)
98
        end,
99
    }
100
end
101

102
while true do
103
    local ok, err = pcall(function()
104
        -- Reset values
105
        slotoffset, handlers, lock = 1, {}, false
106
        local equipment = manipulator.getEquipment() -- {mainhand, offhand, boots, leggings, chest, helmet} <- slot ID to use for each armor slot
107
        local list = equipment.list()
108

109
        createSlotHandler(equipment, 5, config.elytra_id, function(isWearingElytra, swap) -- Elytra
110
            local air_ticks = 0
111
            while true do
112
                if lock then
113
                    os.pullEvent("armor_lock")
114
                end
115
                local meta = manipulator.getMetaOwner()
116
                if meta.isAirborne then
117
                    air_ticks = air_ticks+1
118
                else
119
                    air_ticks = 0
120
                end
121
                local wearingElytra = isWearingElytra()
122
                if (not wearingElytra and meta.heldItemSlot ~= config.held_slot and (air_ticks == config.air_ticks or meta.isSwimming)) or (wearingElytra and not (meta.isAirborne or meta.isSwimming or meta.isSneaking) and not meta.isElytraFlying) then
123
                    swap()
124
                end
125
            end
126
        end)
127

128
        -- Change 3 to 6, and "minecraft:gold_boots" to "minecraft:gold_helmet" if you want to swap the helmet instead
129
        createSlotHandler(equipment, 3, "minecraft:gold_boots", function(isWearingGold, swap)  -- Boots
130
            while true do
131
                if lock then
132
                    os.pullEvent("armor_lock")
133
                end
134
                local _, user, origin, destination = os.pullEvent("world_change")
135
                if user == config.username then
136
                    local wearingGold = isWearingGold()
137
                    if (destination == "minecraft:the_nether" and not wearingGold) or (origin == "minecraft:the_nether" and wearingGold) then
138
                        swap()
139
                    end
140
                end
141
            end
142
        end)
143
        -- Add your own swap handler methods here. Handle swap conditions by passing a function into the waitForAny method below.
144

145
        chatbox.tell(config.username, "&aactive", "armorlock", nil, "format")
146

147
        local threads = {} -- Break out the handler functions to be thrown into parallel
148
        for _, functions in ipairs(handlers) do
149
            threads[#threads+1] = functions[3]
150
        end
151
        parallel.waitForAny(
152
            function() -- Disconnect handler
153
                while true do
154
                    local _, user = os.pullEvent("leave")
155
                    if user == config.username then
156
                        return
157
                    end
158
                end
159
            end,
160
            function() -- Armor lock toggle handler
161
                while true do
162
                    local _, user, command, args, data = os.pullEvent("command")
163
                    if user == config.username and command == "armorlock" then
164
                        lock = not lock
165
                        os.queueEvent("armor_lock")
166
                        for _, functions in ipairs(handlers) do
167
                            if functions[1]() ~= lock then
168
                                functions[2]()
169
                            end
170
                        end
171
                    end
172
                end
173
            end,
174
            table.unpack(threads)
175
        )
176
    end) -- The above functions have a high potential of erroring on player death or disconnect
177
    if not ok then
178
        printError(err)
179
        -- If you want to get a notification in chat of every error, uncomment the following line:
180
        -- chatbox.tell(config.username, "Error:\n&c"..tostring(err), "armorlock", nil, "format")
181
    end
182
    -- Check the playerlist first to make sure the player is still online (probably dead)
183
    local list = chatbox.getPlayerList()
184
    for i, v in ipairs(list) do
185
        list[v] = i
186
    end
187
    if not list[config.username] then -- If they're not online, wait for them to come back.
188
        while true do
189
            local _, user = os.pullEvent("join")
190
            if user == config.username then
191
                break
192
            end
193
        end
194
    else -- If they're presumably dead, wait a second before running again.
195
        sleep(1)
196
    end
197
    handlers = {}
198
end
199