-
Notifications
You must be signed in to change notification settings - Fork 199
/
Copy pathchange-build-menu.lua
575 lines (479 loc) · 18.6 KB
/
change-build-menu.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
-- Edit the build mode sidebar menus.
--
-- Based on my script for the Rubble addon "Libs/Change Build List".
--[[
Copyright 2016 Milo Christiansen
This software is provided 'as-is', without any express or implied warranty. In
no event will the authors be held liable for any damages arising from the use of
this software.
Permission is granted to anyone to use this software for any purpose, including
commercial applications, and to alter it and redistribute it freely, subject to
the following restrictions:
1. The origin of this software must not be misrepresented; you must not claim
that you wrote the original software. If you use this software in a product, an
acknowledgment in the product documentation would be appreciated but is not
required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
]]
--@ module = true
--@ enable = true
local usage = [====[
modtools/change-build-menu
==========================
Change the build sidebar menus.
This script provides a flexible and comprehensive system for adding and removing
items from the build sidebar menus. You can add or remove workshops/furnaces by
text ID, or you can add/remove ANY building via a numeric building ID triplet.
Changes made with this script do not survive a save/load. You will need to redo
your changes each time the world loads.
Just to be clear: You CANNOT use this script AT ALL if there is no world
loaded!
**Usage:**
``modtools/change-build-menu start|enable``:
``enable modtools/change-build-menu``:
Start the ticker. This needs to be done before any changes will take
effect. Note that you can make changes before or after starting the
ticker, both options should work equally well.
``modtools/change-build-menu stop|disable``:
``disable modtools/change-build-menu``:
Stop the ticker. Does not clear stored changes. The ticker will
automatically stop when the current world is unloaded.
``modtools/change-build-menu add <ID> <CATEGORY> [<KEY>]``:
Add the workshop or furnace with the ID ``<ID>`` to ``<CATEGORY>``.
``<KEY>`` is an optional DF hotkey ID.
``<CATEGORY>`` may be one of:
- MAIN_PAGE
- SIEGE_ENGINES
- TRAPS
- WORKSHOPS
- FURNACES
- CONSTRUCTIONS
- MACHINES
- CONSTRUCTIONS_TRACK
Valid ``<ID>`` values for hardcoded buildings are as follows:
- CARPENTERS
- FARMERS
- MASONS
- CRAFTSDWARFS
- JEWELERS
- METALSMITHSFORGE
- MAGMAFORGE
- BOWYERS
- MECHANICS
- SIEGE
- BUTCHERS
- LEATHERWORKS
- TANNERS
- CLOTHIERS
- FISHERY
- STILL
- LOOM
- QUERN
- KENNELS
- ASHERY
- KITCHEN
- DYERS
- TOOL
- MILLSTONE
- WOOD_FURNACE
- SMELTER
- GLASS_FURNACE
- MAGMA_SMELTER
- MAGMA_GLASS_FURNACE
- MAGMA_KILN
- KILN
``modtools/change-build-menu remove <ID> <CATEGORY>``:
Remove the workshop or furnace with the ID ``<ID>`` from ``<CATEGORY>``.
``<CATEGORY>`` and ``<ID>`` may have the same values as for the "add"
option.
``modtools/change-build-menu revert <ID> <CATEGORY>``:
Revert an earlier remove or add operation. It is NOT safe to "remove"
an "add"ed building or vice versa, use this option to reverse any
changes you no longer want/need.
**Module Usage:**
To use this script as a module put the following somewhere in your own script:
.. code-block:: lua
local buildmenu = reqscript "change-build-menu"
Then you can call the functions documented here like so:
- Example: Remove the carpenters workshop:
.. code-block:: lua
buildmenu.ChangeBuilding("CARPENTERS", "WORKSHOPS", false)
- Example: Make it impossible to build walls (not recommended!):
.. code-block:: lua
local typ, styp = df.building_type.Construction, df.construction_type.Wall
buildmenu.ChangeBuildingAdv(typ, styp, -1, "CONSTRUCTIONS", false)
Note that to allow any of your changes to take effect you need to start the
ticker. See the "Command Usage" section.
**Global Functions:**
``GetWShopID(btype, bsubtype, bcustom)``:
GetWShopID returns a workshop's or furnace's string ID based on its
numeric ID triplet. This string ID *should* match what is expected
by eventful for hardcoded buildings.
``GetWShopType(id)``:
GetWShopIDs returns a workshop or furnace's ID numbers as a table.
The passed in ID should be the building's string identifier, it makes
no difference if it is a custom building or a hardcoded one.
The return table is structured like so: ``{type, subtype, custom}``
``IsEntityPermitted(id)``:
IsEntityPermitted returns true if DF would normally allow you to build
a workshop or furnace. Use this if you want to change a building, but
only if it is permitted in the current entity. You do not need to
specify an entity, the current fortress race is used.
``ChangeBuilding(id, category, [add, [key]])``:
``ChangeBuildingAdv(typ, subtyp, custom, category, [add, [key]]):``
These two functions apply changes to the build sidebar menus. If "add"
is true then the building is added to the specified category, else it
is removed. When adding you may specify "key", a string DF hotkey ID.
The first version of this function takes a workshop or furnace ID as a
string, the second takes a numeric ID triplet (which can specify any
building, not just workshops or furnaces).
``RevertBuildingChanges(id, category)``:
``RevertBuildingChangesAdv(typ, subtyp, custom, category)``:
These two functions revert changes made by "ChangeBuilding" and
"ChangeBuildingAdv". Like those two functions there are two versions,
a simple one that takes a string ID and one that takes a numeric ID
triplet.
]====]
-- From here until I say otherwise the APIs are generic building manipulation functions from "Libs/Buildings"
local utils = require "utils"
local wshop_id_to_type = {
CARPENTERS = df.workshop_type.Carpenters,
FARMERS = df.workshop_type.Farmers,
MASONS = df.workshop_type.Masons,
CRAFTSDWARFS = df.workshop_type.Craftsdwarfs,
JEWELERS = df.workshop_type.Jewelers,
METALSMITHSFORGE = df.workshop_type.MetalsmithsForge,
MAGMAFORGE = df.workshop_type.MagmaForge,
BOWYERS = df.workshop_type.Bowyers,
MECHANICS = df.workshop_type.Mechanics,
SIEGE = df.workshop_type.Siege,
BUTCHERS = df.workshop_type.Butchers,
LEATHERWORKS = df.workshop_type.Leatherworks,
TANNERS = df.workshop_type.Tanners,
CLOTHIERS = df.workshop_type.Clothiers,
FISHERY = df.workshop_type.Fishery,
STILL = df.workshop_type.Still,
LOOM = df.workshop_type.Loom,
QUERN = df.workshop_type.Quern,
KENNELS = df.workshop_type.Kennels,
ASHERY = df.workshop_type.Ashery,
KITCHEN = df.workshop_type.Kitchen,
DYERS = df.workshop_type.Dyers,
TOOL = df.workshop_type.Tool,
MILLSTONE = df.workshop_type.Millstone,
}
local wshop_type_to_id = utils.invert(wshop_id_to_type)
local furnace_id_to_type = {
WOOD_FURNACE = df.furnace_type.WoodFurnace,
SMELTER = df.furnace_type.Smelter,
GLASS_FURNACE = df.furnace_type.GlassFurnace,
MAGMA_SMELTER = df.furnace_type.MagmaSmelter,
MAGMA_GLASS_FURNACE = df.furnace_type.MagmaGlassFurnace,
MAGMA_KILN = df.furnace_type.MagmaKiln,
KILN = df.furnace_type.Kiln,
}
local furnace_type_to_id = utils.invert(furnace_id_to_type)
-- GetWShopID returns a workshop or furnace's string ID based on it's numeric ID triplet.
-- This string ID *should* match what is expected by eventful for hardcoded buildings.
--luacheck: in=df.building_type,number,number
function GetWShopID(btype, bsubtype, bcustom)
if btype == df.building_type.Workshop then
if wshop_type_to_id[bsubtype] ~= nil then
return wshop_type_to_id[bsubtype]
else
return df.building_def.find(bcustom).code
end
elseif btype == df.building_type.Furnace then
if furnace_type_to_id[bsubtype] ~= nil then
return furnace_type_to_id[bsubtype]
else
return df.building_def.find(bcustom).code
end
end
end
-- GetWShopIDs returns a workshop or furnace's ID numbers as a table.
-- The passed in ID should be the building's string identifier, it makes
-- no difference if it is a custom building or a hardcoded one.
-- The return table is structured like so: `{type, subtype, custom}`
--luacheck: in=string out={_type:table,type:df.building_type,subtype:number,custom:number}
function GetWShopType(id)
if wshop_id_to_type[id] ~= nil then
-- Hardcoded workshop
return {
type = df.building_type.Workshop,
subtype = wshop_id_to_type[id],
custom = -1,
}
elseif furnace_id_to_type[id] ~= nil then
-- Hardcoded furnace
return {
type = df.building_type.Furnace,
subtype = furnace_id_to_type[id],
custom = -1,
}
else
-- User defined workshop or furnace.
for i, def in ipairs(df.global.world.raws.buildings.all) do
if def.code == id then
local typ = df.building_type.Furnace
local styp = df.furnace_type.Custom
if df.building_def_workshopst:is_instance(def) then --luacheck: skip
typ = df.building_type.Workshop
styp = df.workshop_type.Custom
end
return {
type = typ,
subtype = styp,
custom = i,
}
end
end
end
return nil
end
-- OK, Now to "Libs/Change Build List" proper...
--[[
-- Examples:
-- Remove the carpenters workshop.
ChangeBuilding("CARPENTERS", "WORKSHOPS", false)
-- Make it impossible to build walls (not recommended!).
ChangeBuildingAdv(df.building_type.Construction, df.construction_type.Wall, -1, "CONSTRUCTIONS", false)
-- Add the mechanic's workshops to the machines category.
ChangeBuilding("MECHANICS", "MACHINES", true, "CUSTOM_E")
]]
local category_name_to_id = {
["MAIN_PAGE"] = 0,
["SIEGE_ENGINES"] = 1,
["TRAPS"] = 2,
["WORKSHOPS"] = 3,
["FURNACES"] = 4,
["CONSTRUCTIONS"] = 5,
["MACHINES"] = 6,
["CONSTRUCTIONS_TRACK"] = 7,
}
--[[
{
category = 0, -- The menu category id (from category_name_to_id)
add = true, -- Are we adding a workshop or removing one?
id = {
-- The building IDs.
type = 0,
subtype = 0,
custom = 0,
},
}
]]
stuffToChange = stuffToChange or {} --as:{_type:table,category:number,add:bool,key:df.interface_key,id:{_type:table,type:df.building_type,subtype:number,custom:number}}[]
-- Returns true if DF would normally allow you to build a workshop or furnace.
-- Use this if you want to change a building, but only if it is permitted in the current entity.
--luacheck: in=string out=bool
function IsEntityPermitted(id)
local wshop = GetWShopType(id)
-- It's hard coded, so yes, of course it's permitted, why did you even ask?
if wshop.custom == -1 then
return true
end
local entsrc = df.historical_entity.find(df.global.plotinfo.civ_id)
if entsrc == nil then
return false
end
local entity = entsrc.entity_raw
for _, bid in ipairs(entity.workshops.permitted_building_id) do
if wshop.custom == bid then
return true
end
end
return false
end
function RevertBuildingChanges(id, category)
local wshop = GetWShopType(id)
if wshop == nil then
qerror("RevertBuildingChanges: Invalid workshop ID: "..id)
end
RevertBuildingChangesAdv(wshop.type, wshop.subtype, wshop.custom, category)
end
function RevertBuildingChangesAdv(typ, subtyp, custom, category)
local cat
if tonumber(category) ~= nil then
cat = tonumber(category)
else
cat = category_name_to_id[category]
if cat == nil then
qerror("ChangeBuilding: Invalid category ID: "..category)
end
end
for i = #stuffToChange, 1, -1 do
local change = stuffToChange[i]
if change.category == cat then
if typ == change.id.type and subtyp == change.id.subtype and custom == change.id.custom then
table.remove(stuffToChange, i)
end
end
end
end
function ChangeBuilding(id, category, add, key)
local cat
if tonumber(category) ~= nil then
cat = tonumber(category)
else
cat = category_name_to_id[category]
if cat == nil then
qerror("ChangeBuilding: Invalid category ID: "..category)
end
end
local wshop = GetWShopType(id)
if wshop == nil then
qerror("ChangeBuilding: Invalid workshop ID: "..id)
end
ChangeBuildingAdv(wshop.type, wshop.subtype, wshop.custom, category, add, key)
end
function ChangeBuildingAdv(typ, subtyp, custom, category, add, key)
local cat
if tonumber(category) ~= nil then
cat = tonumber(category)
else
cat = category_name_to_id[category]
if cat == nil then
qerror("ChangeBuilding: Invalid category ID: "..category)
end
end
if type(key) == 'string' then
if tonumber(key) == nil then
key = df.interface_key[key] --luacheck: retype
else
key = tonumber(key) --luacheck: retype
end
end
key = key or 0
table.insert(stuffToChange, {
category = cat,
add = add,
key = key,
id = {
type = typ,
subtype = subtyp,
custom = custom,
},
})
end
-- Return early if module mode. Note that the ticker will not start in module mode!
-- To start the ticker you need to enable the script first ("enable modtools/change-build-menu").
if moduleMode then return end
if not dfhack.isWorldLoaded() then
dfhack.printerr("change-build-menu: No World Loaded!")
dfhack.print(usage)
return
end
local args = {...}
if dfhack_flags and dfhack_flags.enable then
table.insert(args, dfhack_flags.enable_state and 'enable' or 'disable')
end
tickerOn = tickerOn or false
local tickerStart = false
if #args >= 1 then
if args[1] == 'start' or args[1] == 'enable' then
if not tickerOn then tickerStart = true end
elseif args[1] == 'stop' or args[1] == 'disable' then
tickerOn = false
elseif args[1] == 'add' then
ChangeBuilding(args[2], args[3], true, args[4])
return
elseif args[1] == 'remove' then
ChangeBuilding(args[2], args[3], false)
return
elseif args[1] == 'revert' then
RevertBuildingChanges(args[2], args[3])
return
else
dfhack.print(usage)
return
end
else
dfhack.print(usage)
return
end
-- These two store the values we *think* are in effect, they are used to detect changes.
sidebarLastCat = sidebarLastCat or -1
sidebarIsBuild = sidebarIsBuild or false
local function checkSidebar()
-- Needs to be "frames" so it ticks over while paused.
if tickerOn then
dfhack.timeout(1, "frames", checkSidebar)
end
local sidebar = df.global.game.building
if not sidebarIsBuild and df.global.plotinfo.main.mode ~= df.ui_sidebar_mode.Build then
-- Not in build mode.
return
elseif sidebarIsBuild and df.global.plotinfo.main.mode ~= df.ui_sidebar_mode.Build then
-- Just exited build mode
sidebarIsBuild = false
sidebarLastCat = -1
return
elseif sidebarIsBuild and sidebar.category_id == sidebarLastCat then
-- In build mode, but category has not changed since last frame.
return
end
-- Either we just entered build mode or the category has changed.
sidebarIsBuild = true
sidebarLastCat = sidebar.category_id
-- Changes made here do not persist, they need to be made every time the side bar is shown.
-- Will just deleting stuff cause a memory leak? (probably, but how can it be avoided?)
local stufftoremove = {} --as:number[]
local stufftoadd = {} --as:stuffToChange
for i, btn in ipairs(sidebar.choices_all) do
if df.interface_button_construction_building_selectorst:is_instance(btn) then
for _, change in ipairs(stuffToChange) do
if not change.add and sidebar.category_id == change.category and btn.building_type == change.id.type and btn.building_subtype == change.id.subtype and btn.custom_type == change.id.custom then --hint:df.interface_button_construction_building_selectorst
table.insert(stufftoremove, i)
end
end
end
end
for _, change in ipairs(stuffToChange) do
if sidebar.category_id == change.category and change.add then
table.insert(stufftoadd, change)
end
end
-- Do the actual adding and removing.
-- We need to iterate the list backwards to keep from invalidating the stored indexes.
for x = #stufftoremove, 1, -1 do
-- AFAIK item indexes always match (except for one extra item at the end of "choices_all").
local i = stufftoremove[x]
sidebar.choices_visible:erase(i)
sidebar.choices_all:erase(i)
end
for _, change in ipairs(stufftoadd) do
local button = df.interface_button_construction_building_selectorst:new()
button.hotkey_id = change.key
button.building_type = change.id.type
button.building_subtype = change.id.subtype
button.custom_type = change.id.custom
local last = #sidebar.choices_visible
sidebar.choices_visible:insert(last, button)
sidebar.choices_all:insert(last, button)
end
end
-- The logic for the ticker is much more complicated here than the Rubble version.
-- The Rubble version can lean on the script loader, this has to do all the work itself.
-- Greatly complicating things is that Rubble will only evaluate a module once, whereas a
-- command script may be evaluated many times.
dfhack.onStateChange.ChangeBuildList = function(event)
if event == SC_WORLD_LOADED and not tickerOn and tickerStart then
-- Set ticker flags...
tickerOn = true
tickerStart = false
-- Then start the ticker itself.
checkSidebar()
end
if event == SC_WORLD_UNLOADED and tickerOn then
-- This will kill the ticker next frame.
tickerOn = false
-- The Rubble version doesn't need to worry about this stuff, but we do here...
sidebarLastCat = -1
sidebarIsBuild = false
stuffToChange = nil
end
end
dfhack.onStateChange.ChangeBuildList(SC_WORLD_LOADED)