forked from DFHack/dfhack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript-manager.lua
More file actions
398 lines (349 loc) · 13.7 KB
/
script-manager.lua
File metadata and controls
398 lines (349 loc) · 13.7 KB
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
local _ENV = mkmodule('script-manager')
local utils = require('utils')
---------------------
-- enabled API
-- for each script that can be loaded as a module, calls cb(script_name, env)
function foreach_module_script(cb, preprocess_script_file_fn)
for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do
local files = dfhack.filesystem.listdir_recursive(
script_path, nil, false)
if not files then goto skip_path end
for _,f in ipairs(files) do
if f.isdir or not f.path:endswith('.lua') or
f.path:startswith('.git') or
f.path:startswith('test/') or
f.path:startswith('internal/') then
goto continue
end
if preprocess_script_file_fn then
preprocess_script_file_fn(script_path, f.path)
end
local script_name = f.path:sub(1, #f.path - 4) -- remove '.lua'
local ok, script_env = pcall(reqscript, script_name)
if ok then
cb(script_name, script_env)
end
::continue::
end
::skip_path::
end
end
local enabled_map = nil
local function process_script(env_name, env)
local global_name = 'isEnabled'
local fn = env[global_name]
if not fn then return end
if type(fn) ~= 'function' then
dfhack.printerr(
('error registering %s() from "%s": global' ..
' value is not a function'):format(global_name, env_name))
return
end
enabled_map[env_name] = fn
end
function reload(refresh_active_mod_scripts)
enabled_map = utils.OrderedTable()
local force_refresh_fn = refresh_active_mod_scripts and function(script_path, script_name)
if script_path:find('scripts_modactive') then
local full_path = script_path..'/'..script_name
internal_script = dfhack.internal.scripts[full_path]
if internal_script then
dfhack.internal.scripts[full_path] = nil
end
end
end or nil
foreach_module_script(process_script, force_refresh_fn)
end
local function ensure_loaded()
if not enabled_map then
reload()
end
end
function list()
ensure_loaded()
for name,fn in pairs(enabled_map) do
print(('%21s %-3s'):format(name..':', fn() and 'on' or 'off'))
end
end
---------------------
-- mod paths
-- this perhaps could/should be queried from the Steam API
-- are there any installation configurations where this will be wrong, though?
local WORKSHOP_MODS_PATH = '../../workshop/content/975370/'
local MODS_PATH = dfhack.filesystem.getBaseDir() .. 'mods/'
local INSTALLED_MODS_PATH = dfhack.filesystem.getBaseDir() .. 'data/installed_mods/'
-- last instance of the same version of the same mod wins, so read them in this
-- order (in increasing order of liklihood that players may have made custom
-- changes to the files)
local MOD_PATH_ROOTS = {WORKSHOP_MODS_PATH, MODS_PATH, INSTALLED_MODS_PATH}
-- returns the values of the given list of tags from the info.txt file in the given mod directory
-- if a requested tag includes the string 'NUMERIC_', it will return the numeric value for that tag
-- (e.g. NUMERIC_VERSION will return the numeric version of the mod as a number instead of a string).
function get_mod_info_metadata(mod_path, tags)
local idfile = mod_path .. '/info.txt'
local ok, lines = pcall(io.lines, idfile)
if not ok then return end
if type(tags) ~= 'table' then
tags = {tags}
end
local tag_regexes = {}
for _,tag in ipairs(tags) do
local tag_regex = ('^%%[%s:'):format(tag)
if tag:find('NUMERIC_') then
-- note this doesn't go all the way to the closing brace since some people put
-- non-number characters in here, and DF only reads the leading digits for
-- numeric fields
tag_regex = tag_regex .. '(%d+)'
else
tag_regex = tag_regex .. '([^%]]+)'
end
tag_regexes[tag] = tag_regex
end
local values = {}
for line in lines do
local _,_,info_tag = line:find('^%[([^:]+):')
if not info_tag or not tag_regexes[info_tag] then
goto continue
end
local _,_,value = line:find(tag_regexes[info_tag])
if value then
values[info_tag] = value
end
::continue::
end
return values
end
local function get_mod_id_and_version(path)
local values = get_mod_info_metadata(path, {'ID', 'NUMERIC_VERSION'})
if not values then return end
return values.ID, values.NUMERIC_VERSION
end
local function add_mod_paths(mod_paths, id, base_path, subdir)
local sep = base_path:endswith('/') and '' or '/'
local path = ('%s%s%s'):format(base_path, sep, subdir)
if dfhack.filesystem.isdir(path) then
table.insert(mod_paths, {id=id, path=path})
end
end
function get_mod_paths(installed_subdir, active_subdir)
-- ordered map of mod id -> {handled=bool, versions=map of version -> path}
local mods = utils.OrderedTable()
local mod_paths = {}
-- if a world is loaded, process active mods first, and lock to active version
if dfhack.isWorldLoaded() then
for _,path in ipairs(df.global.world.object_loader.object_load_order_src_dir) do
-- skip vanilla "mods"
if not path:startswith(INSTALLED_MODS_PATH) then goto continue end
local id = get_mod_id_and_version(path)
if not id then goto continue end
mods[id] = {handled=true}
if active_subdir then
add_mod_paths(mod_paths, id, path, active_subdir)
end
add_mod_paths(mod_paths, id, path, installed_subdir)
::continue::
end
end
-- assemble version -> path maps for all (non-handled) mod source dirs
for _,mod_path_root in ipairs(MOD_PATH_ROOTS) do
local files = dfhack.filesystem.listdir_recursive(mod_path_root, 0)
if not files then goto skip_path_root end
for _,f in ipairs(files) do
if not f.isdir then goto continue end
local id, version = get_mod_id_and_version(f.path)
if not id or not version then goto continue end
local mod = ensure_key(mods, id)
if mod.handled then goto continue end
ensure_key(mod, 'versions')[version] = f.path
::continue::
end
::skip_path_root::
end
-- add paths from most recent version of all not-yet-handled mods
for id,v in pairs(mods) do
if v.handled then goto continue end
local max_version, path
for version,mod_path in pairs(v.versions) do
if not max_version or max_version < version then
path = mod_path
max_version = version
end
end
add_mod_paths(mod_paths, id, path, installed_subdir)
::continue::
end
return mod_paths
end
-- returns a list of tables in load order with the following fields:
-- id: mod id
-- name: mod display name
-- version: mod display version
-- numeric_version: numeric mod version
-- path: path to the mod directory
-- vanilla: true if this is a vanilla mod
function get_active_mods()
if not dfhack.isWorldLoaded() then return {} end
local mods = {}
local ol = df.global.world.object_loader
for idx=0,#ol.object_load_order_id-1 do
local path = ol.object_load_order_src_dir[idx]
table.insert(mods, {
id=ol.object_load_order_id[idx].value,
name=ol.object_load_order_name[idx].value,
version=ol.object_load_order_displayed_version[idx].value,
numeric_version=ol.object_load_order_numeric_version[idx],
path=path,
vanilla=path:startswith('data/vanilla/'), -- windows also uses forward slashes
})
end
return mods
end
function get_mod_script_paths()
local paths = {}
for _,v in ipairs(get_mod_paths('scripts_modinstalled', 'scripts_modactive')) do
table.insert(paths, v.path)
end
return paths
end
function getModSourcePath(mod_id)
for _,v in ipairs(get_mod_paths('.')) do
if v.id == mod_id then
-- trim off the final '.'
return v.path:sub(1, #v.path-1)
end
end
end
function getModStatePath(mod_id)
local path = ('dfhack-config/mods/%s/'):format(mod_id)
if not dfhack.filesystem.mkdir_recursive(path) then
error(('failed to create mod state directory: "%s"'):format(path))
end
return path
end
---------------------
-- perf API
local function format_time(ms)
return ('%8d ms (%dm %ds)'):format(ms, ms // 60000, (ms % 60000) // 1000)
end
local function format_relative_time(width, name, ms, rel1_ms, desc1, rel2_ms, desc2)
local fmt = '%' .. tostring(width) .. 's %8d ms (%6.2f%% of %s'
local str = fmt:format(name, ms, (ms * 100) / rel1_ms, desc1)
if rel2_ms then
str = str .. (', %6.2f%% of %s'):format((ms * 100) / rel2_ms, desc2)
end
return str .. ')'
end
local function print_sorted_timers(in_timers, width, rel1_ms, desc1, rel2_ms, desc2)
local sum = 0
local sorted = {}
for name,timer in pairs(in_timers) do
table.insert(sorted, {name=name, ms=timer})
sum = sum + timer
end
table.sort(sorted, function(a, b) return a.ms > b.ms end)
for _, elem in ipairs(sorted) do
if elem.ms > 0 then
print(format_relative_time(width, elem.name, elem.ms, rel1_ms, desc1, rel2_ms, desc2))
end
end
local framework_time = math.max(0, rel1_ms - sum)
print()
print(format_relative_time(width, 'framework', framework_time, rel1_ms, desc1, rel2_ms, desc2))
print(format_relative_time(width, 'all subtimers', sum, rel1_ms, desc1, rel2_ms, desc2))
end
function print_timers()
local summary, em_per_event, em_per_plugin_per_event, update_per_plugin, state_change_per_plugin,
update_lua_per_repeat, overlay_per_widget, zscreen_per_focus = dfhack.internal.getPerfCounters()
local elapsed = summary.elapsed_ms
local total_update_time = summary.total_update_ms
local total_overlay_time = summary.total_overlay_ms
local total_zscreen_time = summary.total_zscreen_ms
print('Summary')
print('-------')
print()
print(('Measuring %s'):format(summary.unpaused_only == 1 and 'unpaused time only' or 'paused and unpaused time'))
print()
print(('%7s %s'):format('elapsed', format_time(elapsed)))
if elapsed <= 0 then return end
local sum = summary.total_keybinding_ms + total_update_time + total_overlay_time + total_zscreen_time
print(format_relative_time(7, 'dfhack', sum, elapsed, 'elapsed'))
if sum > 0 then
print()
print()
print('DFHack details')
print('--------------')
print()
print(format_relative_time(10, 'keybinding', summary.total_keybinding_ms, sum, 'dfhack', elapsed, 'elapsed'))
print(format_relative_time(10, 'update', total_update_time, sum, 'dfhack', elapsed, 'elapsed'))
print(format_relative_time(10, 'overlay', total_overlay_time, sum, 'dfhack', elapsed, 'elapsed'))
print(format_relative_time(10, 'zscreen', total_zscreen_time, sum, 'dfhack', elapsed, 'elapsed'))
end
if total_update_time > 0 then
print()
print()
print('Update details')
print('--------------')
print()
print(format_relative_time(15, 'event manager', summary.update_event_manager_ms, total_update_time, 'update', elapsed, 'elapsed'))
print(format_relative_time(15, 'plugin onUpdate', summary.update_plugin_ms, total_update_time, 'update', elapsed, 'elapsed'))
print(format_relative_time(15, 'lua timers', summary.update_lua_ms, total_update_time, 'update', elapsed, 'elapsed'))
end
if summary.update_event_manager_ms > 0 then
print()
print()
print('Event manager per event type')
print('----------------------------')
print()
print_sorted_timers(em_per_event, 25, summary.update_event_manager_ms, 'event manager', elapsed, 'elapsed')
for k,v in pairs(em_per_plugin_per_event) do
if em_per_event[k] <= 0 then goto continue end
print()
print()
local title = ('Event manager %s event per plugin'):format(k)
print(title)
print(('-'):rep(#title))
print()
print_sorted_timers(v, 25, em_per_event[k], 'event type', elapsed, 'elapsed')
::continue::
end
end
if summary.update_plugin_ms > 0 then
print()
print()
print('Update per plugin')
print('-----------------')
print()
print_sorted_timers(update_per_plugin, 25, summary.update_plugin_ms, 'update', elapsed, 'elapsed')
print()
print()
print('State change per plugin')
print('-----------------------')
print()
print_sorted_timers(state_change_per_plugin, 25, summary.update_plugin_ms, 'update', elapsed, 'elapsed')
end
if summary.update_lua_ms > 0 then
print()
print()
print('Lua timer details')
print('-----------------')
print()
print_sorted_timers(update_lua_per_repeat, 45, summary.update_lua_ms, 'lua timers', elapsed, 'elapsed')
end
if total_overlay_time > 0 then
print()
print()
print('Overlay details')
print('---------------')
print()
print_sorted_timers(overlay_per_widget, 45, total_overlay_time, 'overlay', elapsed, 'elapsed')
end
if total_zscreen_time > 0 then
print()
print()
print('ZScreen details')
print('---------------')
print()
print_sorted_timers(zscreen_per_focus, 45, total_zscreen_time, 'zscreen', elapsed, 'elapsed')
end
end
return _ENV