-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontrol.lua
More file actions
427 lines (380 loc) · 17.6 KB
/
Copy pathcontrol.lua
File metadata and controls
427 lines (380 loc) · 17.6 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
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
local flib_dictionary = require "__flib__/dictionary"
local flib_gui = require "__flib__/gui"
local fs_log = require "fs_log"
local fs_util = require "fs_util"
local save = require "manage/save"
local pre_solve = require "manage/pre_solve"
local virtual = require "manage/virtual"
local dictionary = require "manage/dictionary"
local common = require "ui/common"
local main_window = require "ui/main_window"
local build_assistant = require "ui/build_assistant"
local picker_build = require "ui/picker_build"
-- factoriomod-debug injects __DebugAdapter as a truthy global only when the
-- VM is running under the debugger. The injection happens before any mod
-- code loads, so taking a local snapshot here is safe and keeps LuaLS from
-- flagging the global as undefined.
local __DebugAdapter = _G["__DebugAdapter"]
-- Activate the RCON-driven smoke driver only when this Factorio instance was
-- launched as a server with the factory_solver/smoke_rcon scenario (see
-- tests/smoke_rcon.ps1). The mod's normal flow is untouched otherwise. No
-- on_player_created / on_tick hook is needed: the solver pump in on_tick already
-- runs force-scoped, and the launcher drives setup / polling over RCON. We just
-- expose the remote interface it calls. remote.add_interface is not persisted,
-- so it must run on every load -- the control.lua main chunk does, which is
-- where we are.
if script.level and script.level.mod_name == "factory_solver"
and script.level.level_name == "smoke_rcon"
then
require("manage/smoke_rcon").register()
-- Random-chain explorer (tests/chain_explorer.lua) shares this scenario so
-- the same launcher mod-set control and RCON transport can drive it; its
-- interface is separate (factory_solver_explore) from the smoke driver's.
-- It lives under tests/ (harness-only, never loaded outside this gate), so
-- the require is lazy and resolves from the mod root just like manage/*.
require("tests/chain_explorer").register()
-- Headless script context (no __DebugAdapter): default to debug so the RCON
-- tooling reads back create_problem's debug-tier reproduction data, the same
-- verbosity a debugger session gets. Trace (the bulky LP internals) stays
-- opt-in via the /factory-solver-log-level command.
fs_log.set_level("debug")
end
---Open the docked Build assistant for a player unless it is already shown.
---Called at new-game / new-player time so the early-game Manual mode is visible
---from the start; the panel is part of the saved GUI tree, so it survives
---save/load, and the shortcut still toggles it off afterwards.
---@param player LuaPlayer
local function open_build_assistant_default(player)
if player.gui.left["factory_solver_build_assistant"] == nil then
fs_util.add_gui(player.gui.left, build_assistant)
end
end
script.on_init(function()
flib_dictionary.on_init()
storage.virtuals = virtual.create_virtuals()
-- Locale dictionaries for the constraint-adder name filter. Must be built
-- here (and in on_configuration_changed), before flib's first on_tick marks
-- the dictionary module initialized.
dictionary.build()
-- New saves are born on the engine-aligned layout default, so they never get
-- the one-time "the default layout changed" notice. Pre-set the flag here;
-- existing saves (which reach on_configuration_changed instead) leave it nil
-- and get the notice once on first load after the update.
storage.layout_default_notice_shown = true
-- Force data must exist before any GUI is built: open_build_assistant_default
-- renders the docked panel, whose make_build_table handler immediately reads
-- storage.forces via save.get_selected_solution. When the mod is added to an
-- existing save (players already present), the player loop below runs for real,
-- so storage.forces has to be ready first. (In a fresh world game.players is
-- empty at on_init time, which is why this ordering bug stayed hidden.)
storage.forces = {}
for _, force in pairs(game.forces) do
save.init_force_data(force.index)
if __DebugAdapter then
for _, quality in pairs(prototypes.quality) do
force.unlock_quality(quality)
end
end
end
storage.players = {}
for _, player in pairs(game.players) do
save.init_player_data(player.index)
open_build_assistant_default(player)
if __DebugAdapter then
player.cheat_mode = true
end
end
if __DebugAdapter then
if remote.interfaces["freeplay"] then
remote.call("freeplay", "set_skip_intro", true)
remote.call("freeplay", "set_disable_crashsite", true)
end
end
end)
script.on_load(function()
for _, force in pairs(storage.forces) do
save.resetup_force_data_metatable(force)
end
end)
script.on_configuration_changed(function(event)
flib_dictionary.on_configuration_changed()
storage.virtuals = virtual.create_virtuals()
-- flib resets its dictionary storage on on_configuration_changed, so the
-- name-filter dictionaries must be re-declared here too.
dictionary.build()
-- One-time chat notice for existing saves: the layout default flipped to
-- follow Factorio's ingredient-to-product order. Guarded by a storage flag so
-- it prints exactly once per save (new saves pre-set it in on_init). The flag
-- is nil on saves created before this change, which is exactly who should be
-- told. Version-drift-proof: no version comparison needed.
if not storage.layout_default_notice_shown then
storage.layout_default_notice_shown = true
game.print({ "factory-solver-layout-default-changed" })
end
for _, player in pairs(game.players) do
save.reinit_player_data(player.index)
local player_data = storage.players[player.index]
local screen = player.gui.screen
for _, name in ipairs(player_data.opened_gui) do
if screen[name] then
screen[name].destroy()
end
end
player_data.opened_gui = {}
player.set_shortcut_toggled("factory-solver-toggle-main-window", false)
-- The build assistant is a docked panel in gui.left, outside the
-- opened_gui modal stack, so it is destroyed and untoggled explicitly.
if player.gui.left["factory_solver_build_assistant"] then
player.gui.left["factory_solver_build_assistant"].destroy()
end
player.set_shortcut_toggled("factory-solver-toggle-build-assistant", false)
end
for _, force in pairs(game.forces) do
save.reinit_force_data(force.index)
end
end)
script.on_event(defines.events.on_player_created, function(event)
save.init_player_data(event.player_index)
local player = game.players[event.player_index]
open_build_assistant_default(player)
if __DebugAdapter then
player.cheat_mode = true
end
end)
script.on_event(defines.events.on_player_changed_force, function(event)
save.init_force_data(event.force.index)
if __DebugAdapter then
for _, quality in pairs(prototypes.quality) do
event.force.unlock_quality(quality)
end
end
end)
script.on_event(defines.events.on_player_removed, function(event)
storage.players[event.player_index] = nil
end)
script.on_event(defines.events.on_force_created, function(event)
save.init_force_data(event.force.index)
end)
script.on_event(defines.events.on_force_reset, function(event)
save.reinit_force_data(event.force.index)
end)
script.on_event(defines.events.on_forces_merged, function(event)
local destination_force_data = storage.forces[event.destination.index]
local source_force_data = storage.forces[event.source_index]
destination_force_data.solutions = fs_util.array_merge {
destination_force_data.solutions,
source_force_data.solutions
}
storage.forces[event.source_index] = nil
end)
---The recipe names a technology unlocks, from its unlock-recipe effects. Drives
---the incremental relation_to_recipes update so a research finish/reverse patches
---only the affected recipes instead of rebuilding the whole cache.
---@param research LuaTechnology
---@return string[]
local function unlocked_recipe_names(research)
local names = {}
for _, effect in ipairs(research.prototype.effects) do
if effect.type == "unlock-recipe" then
names[#names + 1] = effect.recipe
end
end
return names
end
script.on_event(defines.events.on_research_finished, function(event)
save.apply_research_change(event.research.force.index,
unlocked_recipe_names(event.research), true)
end)
script.on_event(defines.events.on_research_reversed, function(event)
save.apply_research_change(event.research.force.index,
unlocked_recipe_names(event.research), false)
end)
script.on_event(defines.events.on_runtime_mod_setting_changed, function(event)
if event.setting ~= "factory-solver-classic-recipe-io-placement"
and event.setting ~= "factory-solver-classic-production-line-order" then
return
end
-- These per-player display settings are read at GUI build time, so
-- re-dispatching the production-line rebuild event applies them live (no
-- re-solve — that path runs in on_tick). The reverse-order setting also
-- affects the build assistant rows and the results-panel section order, so
-- refresh both windows (the editor table, results panel and BA table all
-- listen to on_production_line_changed). Only the toggling player needs it;
-- the add-production-line dialog re-reads the setting whenever it is opened.
local player_index = event.player_index
if not player_index then
return
end
local player = game.players[player_index]
local window = player.gui.screen["factory_solver_main_window"]
if window then
fs_util.dispatch_to_subtree(window, "on_production_line_changed")
end
local build_window = player.gui.left["factory_solver_build_assistant"]
if build_window then
fs_util.dispatch_to_subtree(build_window, "on_production_line_changed")
end
end)
-- The main window is sized to fill the whole display (see ui/main_window.lua),
-- which is computed from the player's resolution / UI scale at build time. When
-- either changes while the window is open it would otherwise keep the old size
-- and overflow (or underfill) the screen, so refit it live. Only the affected
-- player's window needs it.
local function refit_main_window(player_index)
local player = game.players[player_index]
local window = player.gui.screen["factory_solver_main_window"]
if window then
common.resize_window_to_screen(player, window)
end
end
script.on_event(defines.events.on_player_display_resolution_changed, function(event)
refit_main_window(event.player_index)
end)
script.on_event(defines.events.on_player_display_scale_changed, function(event)
refit_main_window(event.player_index)
end)
script.on_event(defines.events.on_tick, function(event)
flib_dictionary.on_tick()
-- Advance the tick-split relation build (manage/relation.lua) before solving so
-- a GUI reading the cache this tick sees as much progress as possible; the
-- synchronous fallback in save.get_relation_to_recipes covers the rest. It is
-- independent of the solver (the solve path never reads relation_to_recipes), so
-- both run every tick.
save.advance_relation_builds()
-- Advance the tick-split recipe-picker build (ui/picker_build.lua) -- a large
-- picker would otherwise build thousands of buttons in one tick, which under
-- lockstep freezes every client. One section advances per tick; PLAN may read
-- the relation cache, so run it after advance_relation_builds.
picker_build.advance_all()
local force_data, solution = pre_solve.find_the_need_for_solve()
if force_data and solution then
pre_solve.forwerd_solve(force_data, solution)
for _, player in pairs(game.players) do
local window = player.gui.screen["factory_solver_main_window"]
if window then
fs_util.dispatch_to_subtree(window, "on_calculation_changed")
end
local build_window = player.gui.left["factory_solver_build_assistant"]
if build_window then
fs_util.dispatch_to_subtree(build_window, "on_calculation_changed")
end
end
end
end)
---@param player_index integer
local function toggle_main_window(player_index)
local player = game.players[player_index]
local window = player.gui.screen["factory_solver_main_window"]
if window == nil then
common.open_gui(player_index, false, main_window)
else
common.on_close_self {
element = window,
name = "on_close_toggle",
player_index = player_index,
tick = game.tick,
mod_name = window.get_mod()
}
end
end
-- The build assistant is a persistent, docked panel rather than a floating
-- window: it lives in gui.left so it sits beside the main window instead of
-- hiding behind it, and it is added/destroyed directly rather than through
-- common.open_gui (which targets gui.screen, pushes onto the opened_gui modal
-- stack, and sets player.opened — none of which fit a docked panel).
---@param player_index integer
local function toggle_build_assistant(player_index)
local player = game.players[player_index]
local window = player.gui.left["factory_solver_build_assistant"]
if window == nil then
fs_util.add_gui(player.gui.left, build_assistant)
else
fs_util.dispatch_to_subtree(window, "on_close")
window.destroy()
end
end
---@param event EventData.CustomInputEvent
script.on_event("factory-solver-toggle-main-window", function(event)
toggle_main_window(event.player_index)
end)
---@param event EventData.CustomInputEvent
script.on_event("factory-solver-toggle-build-assistant", function(event)
toggle_build_assistant(event.player_index)
end)
script.on_event(defines.events.on_lua_shortcut, function(event)
if event.prototype_name == "factory-solver-toggle-main-window" then
toggle_main_window(event.player_index)
elseif event.prototype_name == "factory-solver-toggle-build-assistant" then
toggle_build_assistant(event.player_index)
end
end)
-- Lets a player (or the server console) raise the fs_log threshold below
-- `debug` so the A-matrix dump in solver.lp's "ready" block reaches the log.
-- Intended for fixture capture: turn trace on, trigger a solve, copy the
-- emitted cost/limit/subject block into a tests/cases/*.lua entry, turn it
-- back off. Threshold lives in fs_log's module-local state (not storage), so
-- the change is per-process and does not need to survive save/load; running
-- the command on every client in MP keeps log threshold consistent across
-- clients without making it desync-relevant.
commands.add_command(
"factory-solver-log-level",
"Set the fs_log threshold (trace | debug | info | warn | error). " ..
"With no argument, prints the current level.",
function(event)
local sink = event.player_index
and game.players[event.player_index]
or game
local arg = event.parameter and event.parameter:match("^%s*(%S+)%s*$")
if not arg then
sink.print("factory_solver log level: " .. fs_log.get_level())
return
end
local ok = pcall(fs_log.set_level, arg)
if ok then
sink.print("factory_solver log level set to " .. arg)
else
sink.print("factory_solver: unknown log level '" .. arg ..
"' (expected trace | debug | info | warn | error)")
end
end
)
-- The amount quick-set popup (ui/solution_settings.lua) is an inline element, so
-- it can't use a modal backdrop to detect outside clicks (the backdrop would
-- cover the popup itself). Register on_gui_click *before* flib_gui.handle_events()
-- -- which skips ids that already have a handler -- and forward to
-- flib_gui.dispatch ourselves, then close the popup when the click lands outside
-- it. This only sees GuiElement clicks (not world / empty-GUI clicks), an
-- accepted limitation.
script.on_event(defines.events.on_gui_click, function(event)
flib_gui.dispatch(event)
local player_data = storage.players[event.player_index]
if not (player_data and player_data.expanded_constraint_index) then return end
local element = event.element
if not (element and element.valid) then return end
-- The amount field (any row) and anything inside the popup keep it open: the
-- field stays editable; the slider / throughput / close button handle
-- themselves.
if element.name == "amount_field" then return end
if fs_util.find_upper(element, "popup") then return end
-- Outside click: clear the marker and hide the popups in place. We must NOT
-- rebuild the table here -- the click may be on the limit dropdown (or another
-- in-table control), and a rebuild would destroy that element mid-interaction
-- (closing the dropdown the user just opened). The slider / throughput
-- handlers already mirror their value into the row textfield, so nothing
-- needs refreshing on close.
player_data.expanded_constraint_index = nil
local root = common.find_root_element(event.player_index, "factory_solver_main_window")
if root then
local list = fs_util.find_lower(root, "constraint_list")
if list then
for _, row in pairs(list.children) do
local popup = row["popup"]
if popup then
popup.visible = false
end
end
end
end
end)
flib_dictionary.handle_events()
flib_gui.handle_events()