forked from anonhostpi/mk-fg-games
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathcontrol.lua
More file actions
1147 lines (1006 loc) · 41.6 KB
/
Copy pathcontrol.lua
File metadata and controls
1147 lines (1006 loc) · 41.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
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
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
local conf = require('config')
conf.update_from_settings()
local guis = require('gui')
-- Stores code and built environments as {code=..., ro=..., vars=...}
-- This stuff can't be global (kept in `storage`?), built locally, might be cause for desyncs
local Combinators = {}
local CombinatorEnv = {} -- to avoid self-recursive tables
local function tt(s, value)
-- Helper to make padded table from other table keys or a string of keys
local t = {}
if not value then value = true end
if type(s) == 'table' then for k,_ in pairs(s) do t[k] = value end
else s:gsub('(%S+)', function(k) t[k] = value end) end
return t
end
local function tc(src)
-- Shallow-copy a table with keys
local t = {}
for k, v in pairs(src) do t[k] = v end
return t
end
local function tdc(object)
-- Deep-copy of lua table, from factorio util.lua
local lookup_table = {}
local function _copy(object)
if type(object) ~= 'table' then return object
elseif object.__self then return object
elseif lookup_table[object] then return lookup_table[object] end
local new_table = {}
lookup_table[object] = new_table
for index, value in pairs(object)
do new_table[_copy(index)] = _copy(value) end
return setmetatable(new_table, getmetatable(object))
end
return _copy(object)
end
local function console_warn(p, text)
p.print(('[Moon Logic mod] %s'):format(text), {0.957, 0.710, 0.659})
end
-- ----- Circuit network controls -----
local function cn_sig_quality(sig_str)
-- removes quality prefix from signal string, returns signal name and quality
if not sig_str then return end
local q_name = ""
for _, q in pairs(storage.quality) do
if string.match(sig_str, q.."/") then
q_name = q
break
end
end
if q_name == "" then return sig_str end
return string.gsub(sig_str, q_name.."/", ''), q_name
end
local cn_sig_str_prefix = {item='#', fluid='=', virtual='@', recipe='~'}
local function cn_sig_str(t, name)
-- Translates name or type/name or signal to its type-prefixed string-id
if not name then
if type(t) == 'string' then
name = storage.signals_short[t]
if name == false then return end -- ambiguous name
return name or t
else
if t.type == nil then t.type = 'item' end
name = t.name
end
end
if not t then
return storage.signals_short[name]
end
if t.quality ~= "normal" and t.quality ~= nil then
return t.quality.."/"..cn_sig_str_prefix[t.type]..name
end
return cn_sig_str_prefix[t.type or t]..name
end
local function cn_sig_name(sig_str)
-- Returns abbreviated signal name without prefix
local signame, qname = cn_sig_quality(sig_str)
local k = signame:sub(2)
local sig_str2 = storage.signals_short[k]
if sig_str2 == false then return sig_str
elseif sig_str2 == sig_str then return k
elseif qname ~= nil and qname.."/"..sig_str2 == sig_str then return qname.."/"..k
elseif not sig_str2 then error(('MOD BUG - abbreviation for invalid signal string: %s'):format(sig_str))
else error(('MOD BUG - signal string/abbrev mismatch: %s != %s'):format(sig_str, sig_str2)) end
end
local function cn_sig(k, err_level)
local signame, qname = cn_sig_quality(k)
local sig = storage.signals_short[signame or k]
if type(sig) ~= false then sig = storage.signals[sig or signame] end
if sig and qname ~="" then
sig.quality = qname
return sig
elseif sig then
return sig
end
if not err_level then return end
if sig == false then
local m = {}
for _,t in ipairs{'virtual', 'item', 'fluid'} do
sig = cn_sig_str(t, k)
if storage.signals[sig] then table.insert(m, sig) end
end
error(( 'Ambiguous short signal name "%s",'..
' matching: %s' ):format(k, table.concat(m, ' ')), err_level)
end
error('Unknown signal: '..k, err_level)
end
local function cn_wire_signals(e, wire_type, canon)
-- Returns signal=count table, with signal names abbreviated where possible
local ccid
if wire_type == defines.wire_type.green then
ccid = defines.wire_connector_id.combinator_input_green
elseif wire_type == defines.wire_type.red then
ccid = defines.wire_connector_id.combinator_input_red
end
local res, cn, k = {}, e.get_or_create_control_behavior()
.get_circuit_network(ccid)
for _, sig in pairs(cn and cn.signals or {}) do
-- Check for name=nil SignalIDs (dunno what these are), and items w/ flag=hidden
if storage.signals_short[sig.signal.name] == nil then goto skip end
if canon then k = cn_sig_str(sig.signal)
elseif sig.signal.quality ~= nil and sig.signal.type == 'recipe' then
k = sig.signal.quality.."/~"..storage.signals_short[sig.signal.name] and sig.signal.quality.."/~"..sig.signal.name or "~"..cn_sig_str(sig.signal)
elseif sig.signal.type == 'recipe' then
k = "~"..storage.signals_short[sig.signal.name] and "~"..sig.signal.name or "~"..cn_sig_str(sig.signal)
elseif sig.signal.quality ~= nil then
k = sig.signal.quality.."/"..storage.signals_short[sig.signal.name] and sig.signal.quality.."/"..sig.signal.name or cn_sig_str(sig.signal)
else k = storage.signals_short[sig.signal.name]
and sig.signal.name or cn_sig_str(sig.signal) end
res[k] = sig.count
::skip:: end
return res
end
local function cn_input_signal(wenv, wire_type, k)
local signals = wenv._cache
if wenv._cache_tick ~= game.tick then
signals = cn_wire_signals(wenv._e, wire_type, true)
wenv._cache, wenv._cache_tick = signals, game.tick
end
if k and #storage.quality ~= 0 then
local signame, q_name = cn_sig_quality(k)
if q_name == nil or q_name == "normal" then
signals = signals[cn_sig_str(cn_sig(signame, 4))]
return signals
end
signals = signals[q_name.."/"..cn_sig_str(cn_sig(signame, 4))]
return signals
end
if k then signals = signals[cn_sig_str(cn_sig(k, 4))] end
return signals
end
local function cn_input_signal_get(wenv, k)
local v = cn_input_signal(wenv, defines.wire_type[wenv._wire], k) or 0
if wenv._debug then wenv._debug[conf.get_wire_label(wenv._wire)..'['..k..']'] = v end
return v
end
local function cn_input_signal_set(wenv, k, v)
error(( 'Attempt to set value on input wire:'..
' %s[%s] = %s' ):format(conf.get_wire_label(wenv._wire), k, v), 2)
end
local function cn_input_signal_len(wenv)
local n, sigs = 0, cn_input_signal(wenv, defines.wire_type[wenv._wire])
for sig, c in pairs(sigs) do if c ~= 0 then n = n + 1 end end
return n
end
local function cn_input_signal_iter(wenv)
-- This returns shortened signal names for simplicity and compatibility
local signals, sig_cache = {}, cn_input_signal(wenv, defines.wire_type[wenv._wire])
for k, v in pairs(sig_cache) do signals[cn_sig_name(k)] = sig_cache[k] end
if wenv._debug then
local sig_fmt = conf.get_wire_label(wenv._wire)..'[%s]'
for sig, v in pairs(signals) do wenv._debug[sig_fmt:format(sig)] = v or 0 end
end
return signals
end
local function cn_input_signal_table_serialize(wenv)
return {__wire_inputs=conf.get_wire_label(wenv._wire)}
end
local function cn_output_table_len(out) -- rawlen won't skip 0 and doesn't work anyway
local n = 0
for k, v in pairs(out) do if v ~= 0 then n = n + 1 end end
return n
end
local function cn_output_table_value(out, k)
if k == '__self' then return end -- for table.deepcopy to tell this apart from factorio object
return rawget(out, k) or rawget(out, storage.signals_short[k]) or 0
end
local function cn_output_table_replace(out, new_tbl)
-- Note: validation for sig_names/values is done when output table is used later
for sig, v in pairs(out) do out[sig] = nil end
for sig, v in pairs(new_tbl or {}) do out[sig] = v end
end
-- ----- Sandbox base -----
local sandbox_env_pairs_mt_iter = {}
local function sandbox_env_pairs(tbl) -- allows to iterate over red/green ro-tables
local mt = getmetatable(tbl)
if mt and sandbox_env_pairs_mt_iter[mt.__index] then tbl = tbl._iter(tbl) end
return pairs(tbl)
end
local function sandbox_clean_table(tbl, apply_func)
local tbl_clean = {}
for k, v in sandbox_env_pairs(tbl) do tbl_clean[k] = v end
if apply_func then return apply_func(tbl_clean) else return tbl_clean end
end
local function sandbox_game_print(...)
local args, msg = table.pack(...), ''
for _, arg in ipairs(args) do
if msg ~= '' then msg = msg..' ' end
if type(arg) == 'table' then arg = sandbox_clean_table(arg, serpent.line) end
msg = msg..(tostring(arg or 'nil') or '[value]')
end
game.print(msg)
end
-- This env gets modified on ticks, which might cause mp desyncs
local sandbox_env_base = {
_init = false,
pairs = sandbox_env_pairs,
ipairs = ipairs,
next = next,
pcall = pcall,
tonumber = tonumber,
tostring = tostring,
type = type,
assert = assert,
error = error,
select = select,
serpent = {
block = function(tbl) return sandbox_clean_table(tbl, serpent.block) end,
line = function(tbl) return sandbox_clean_table(tbl, serpent.line) end },
string = {
byte = string.byte, char = string.char, find = string.find,
format = string.format, gmatch = string.gmatch, gsub = string.gsub,
len = string.len, lower = string.lower, match = string.match,
rep = string.rep, reverse = string.reverse, sub = string.sub,
upper = string.upper },
table = {
concat = table.concat, insert = table.insert, remove = table.remove,
sort = table.sort, pack = table.pack, unpack = table.unpack },
math = {
abs = math.abs, acos = math.acos, asin = math.asin,
atan = math.atan, atan2 = math.atan2, ceil = math.ceil, cos = math.cos,
cosh = math.cosh, deg = math.deg, exp = math.exp, floor = math.floor,
fmod = math.fmod, frexp = math.frexp, huge = math.huge,
ldexp = math.ldexp, log = math.log, max = math.max,
min = math.min, modf = math.modf, pi = math.pi, pow = math.pow,
rad = math.rad, random = math.random, sin = math.sin, sinh = math.sinh,
sqrt = math.sqrt, tan = math.tan, tanh = math.tanh },
bit32 = {
arshift = bit32.arshift, band = bit32.band, bnot = bit32.bnot,
bor = bit32.bor, btest = bit32.btest, bxor = bit32.bxor,
extract = bit32.extract, replace = bit32.replace, lrotate = bit32.lrotate,
lshift = bit32.lshift, rrotate = bit32.rrotate, rshift = bit32.rshift }
}
-- ----- MLC update processing -----
local mlc_err_sig = {type='virtual', name='mlc-error'}
local function mlc_update_output(mlc, output_raw)
-- Sets signal outputs on invisible mlc-core combinators connected to visible outputs
local signals, errors, output = {red={}, green={}}, {}, {}
for k, v in pairs(output_raw) do output[tostring(k)] = v end
local sig_err, sig, st, err, pre, pre_label = tc(output)
for _, k in ipairs{false, 'red', 'green'} do
st = signals[k] and {signals[k]} or {signals.red, signals.green}
if not k then pre, pre_label = '^.+$', '^.+$'
else pre, pre_label = '^'..k..'/(.+)$', '^'..conf.get_wire_label(k)..'/(.+)$' end
for k, v in pairs(output) do
sig, err = cn_sig(k:match(pre) or k:match(pre_label))
if not sig then goto skip end
sig_err[k] = nil
if type(v) == 'boolean' then v = v and 1 or 0
elseif type(v) ~= 'number' then
err = ('signal must be a number [%s=(%s) %s]'):format(sig.name, type(v), v)
elseif not (v >= -2147483648 and v <= 2147483647) then
err = ('signal value out of range [%s=%s]'):format(sig.name, v) end
if err then table.insert(errors, err); goto skip end
for _, sig_table in ipairs(st) do sig_table[cn_sig_str(sig)] = v end
::skip:: end end
for sig, _ in pairs(sig_err)
do table.insert(errors, ('unknown signal [%s]'):format(sig)) end
local ps, ecc, n
for _, k in ipairs{'red', 'green'} do
ps, ecc = {}, mlc['out_'..k].get_or_create_control_behavior()
if not (ecc and ecc.valid) then goto skip end
n = 1
for sig, v in pairs(signals[k]) do
local qname = ""
sig,qname = cn_sig_quality(sig)
ps[n] = {value={name = "", quality = "", type = ""}, min=v}
ps[n].value.name = storage.signals[sig].name
ps[n].value.type = storage.signals[sig].type
ps[n].value.quality = qname or "normal"
n = n + 1
end
ecc.enabled = true
ecc.get_section(1).filters = ps
::skip:: end
if next(errors) then mlc.err_out = table.concat(errors, ', ') end
end
local function mlc_update_led(mlc, mlc_env)
-- This should set state in a way that doesn't actually produce any signals
-- Combinator is not considered 'active', as it ends up with 0 value, unless it's mlc-error
-- It's possible to have it output value and cancel it out, but shows-up on ALT-display
-- First constant on the combinator encodes its uid value, as a fallback to copy code in blueprints
if mlc.state == mlc_env._state then return end
local st, cb = mlc.state, mlc.e.get_or_create_control_behavior()
if not (cb and cb.valid) then return end
local op, a, b, out = '*', mlc_env._uid, 0
-- uid is uint, signal is int (signed), so must be negative if >=2^31
if a >= 0x80000000 then a = a - 0x100000000 end
if not st then op = '*'
elseif st == 'run' then op = '%'
elseif st == 'sleep' then op = '-'
elseif st == 'no-power' then op = '^'
elseif st == 'error' then op, b, out = '+', 1 - a, mlc_err_sig end -- shown with ALT
mlc_env._state, cb.parameters = st, {
operation=op, first_signal=nil, second_signal=nil,
first_constant=a, second_constant=b, output_signal=out }
end
local function mlc_update_code(mlc, mlc_env, lua_env)
mlc.next_tick, mlc.state, mlc.err_parse, mlc.err_run, mlc.err_out = 0
local code, err = (mlc.code or '')
if code:match('^%s*(.-)%s*$') ~= '' then
mlc_env._func, err = load(
code, code, 't', lua_env or CombinatorEnv[mlc_env._uid] )
if not mlc_env._func then mlc.err_parse, mlc.state = err, 'error' end
else
mlc_env._func = nil
cn_output_table_replace(mlc_env._out)
mlc_update_output(mlc, mlc_env._out)
end
mlc_update_led(mlc, mlc_env)
end
-- ----- MLC (+ sandbox) init / remove -----
local Terminals = {
red = {
output = defines.wire_connector_id.combinator_output_red,
input = defines.wire_connector_id.combinator_input_red
},
green = {
output = defines.wire_connector_id.combinator_output_green,
input = defines.wire_connector_id.combinator_input_green
}
}
-- Create/connect/remove invisible constant-combinator entities for wire outputs
local function out_wire_connect(e, color)
local core = e.surface.create_entity{
name='mlc-core', position=e.position,
force=e.force, create_build_effect_smoke=false }
local terminals = Terminals[color]
local connectors = {
transmitter = e.get_wire_connector( terminals.output ),
receiver = core.get_wire_connector( terminals.input )
}
local success = connectors.transmitter.connect_to( connectors.receiver, false, defines.wire_origin.script )
if not success then
error(('Failed to connect %s wire outputs to core'):format(color))
end
core.destructible = false
return core
end
local function out_wire_connect_both(e)
return
out_wire_connect(e, "red"),
out_wire_connect(e, "green")
end
local function out_wire_clear_mlc(mlc)
for _, e in ipairs{'core', 'out_red', 'out_green'} do
e, mlc[e] = mlc[e]
if e and e.valid then e.destroy() end
end
return mlc
end
local function out_wire_connect_mlc(mlc)
out_wire_clear_mlc(mlc)
mlc.out_red, mlc.out_green = out_wire_connect_both(mlc.e)
return mlc
end
local function mlc_log(...) log(...) end -- to avoid logging func code
-- Detect Moon-Logic input wire proxies and snapshot them to a plain table
local function is_wire_proxy(v)
if type(v) ~= 'table' then return false end
local mt = getmetatable(v)
-- red/green proxies are metatables with __index = cn_input_signal_get
return mt and mt.__index == cn_input_signal_get
end
local function snapshot_wire_proxy(wenv)
-- Use the proxy's iterator to get a {short_signal_name = count} table
-- (cn_input_signal_iter already shortens names and returns present signals)
local snap, iter_tbl = {}, cn_input_signal_iter(wenv)
for name, val in pairs(iter_tbl) do
if val ~= 0 then snap[name] = val end
end
return snap
end
local function mlc_init(e)
-- Inits *local* mlc_env state for combinator - builds env, evals lua code, etc
-- *storage* (previously `global`) state will be used for init values if it exists, otherwise empty defaults
-- Lua env for code is composed from: sandbox_env_base + local mlc_env proxies + global (storage) mlc.vars
if not e.valid then return end
local uid = e.unit_number
if Combinators[uid] then error('Double-init for existing combinator unit_number') end
Combinators[uid] = {} -- some state (e.g. loaded func) has to be local
if not storage.combinators[uid] then storage.combinators[uid] = {e=e} end
local mlc_env, mlc = Combinators[uid], storage.combinators[uid]
if not sandbox_env_base._init then
-- This is likely to cause mp desyncs
sandbox_env_base.game = {
tick=game.tick, log=mlc_log,
print=sandbox_game_print, print_color=game.print }
sandbox_env_base._api = { game=game, script=script,
remote=remote, commands=commands, settings=settings,
rcon=rcon, rendering=rendering, global=storage, defines=defines, prototypes=prototypes }
sandbox_env_pairs_mt_iter[cn_input_signal_get] = true
sandbox_env_base._init = true
end
mlc.output, mlc.vars = mlc.output or {}, mlc.vars or {}
mlc_env._e, mlc_env._uid, mlc_env._out = e, uid, mlc.output
local env_wire_red = {
_e=mlc_env._e, _wire='red', _debug=false, _out=mlc_env._out,
_iter=cn_input_signal_iter, _cache={}, _cache_tick=-1 }
local env_wire_green = tc(env_wire_red)
env_wire_green._wire = 'green'
local env_ro = { -- sandbox_env_base + mlc_env proxies
uid = mlc_env._uid,
out = setmetatable( mlc_env._out,
{__index=cn_output_table_value, __len=cn_output_table_len} ),
red = setmetatable(env_wire_red, {
__serialize=cn_input_signal_table_serialize, __len=cn_input_signal_len,
__index=cn_input_signal_get, __newindex=cn_input_signal_set }),
green = setmetatable(env_wire_green, {
__serialize=cn_input_signal_table_serialize, __len=cn_input_signal_len,
__index=cn_input_signal_get, __newindex=cn_input_signal_set }) }
env_ro[conf.red_wire_name] = env_ro.red
env_ro[conf.green_wire_name] = env_ro.green
setmetatable(env_ro, {__index=sandbox_env_base})
if not mlc.vars.var then mlc.vars.var = {} end
local env = setmetatable({}, {
__index = function(_, k)
local v = mlc.vars[k]
if v ~= nil then return v end
return env_ro[k]
end,
__newindex = function(_, k, v)
if k == 'out' then
cn_output_table_replace(env_ro.out, v)
rawset(env_wire_red, '_debug', v)
rawset(env_wire_green, '_debug', v)
return
end
-- turn red/green proxies into snapshots
if is_wire_proxy(v) then
v = snapshot_wire_proxy(v)
end
rawset(mlc.vars, k, v)
end
})
mlc_env.debug_wires_set = function(v)
local v_prev = rawget(env_wire_red, '_debug')
rawset(env_wire_red, '_debug', v or false)
rawset(env_wire_green, '_debug', v or false)
return v_prev end
-- Migration from pre-0.0.52 to separate wire outputs
if mlc.core then out_wire_connect_mlc(mlc) end
CombinatorEnv[uid] = env
mlc_update_code(mlc, mlc_env, env)
return mlc_env
end
local function mlc_remove(uid, keep_entities, to_be_mined)
guis.close(uid)
if not keep_entities then
local mlc = out_wire_clear_mlc(storage.combinators[uid] or {})
if not to_be_mined and mlc.e and mlc.e.valid then mlc.e.destroy() end
end
Combinators[uid], CombinatorEnv[uid], storage.combinators[uid], storage.guis[uid] = nil
end
-- ----- Misc events -----
local mlc_filter = {{filter='name', name='mlc'}}
local function blueprint_match_positions(bp_es, map_es)
-- Hack to work around invalidated ev.mapping - match entities by x/y position
-- Same idea as in https://forums.factorio.com/viewtopic.php?p=466734
-- but x/y in blueprints seem to be absolute in current factorio, not offset from center
local bp_mlcs, bp_mlc_uids, be, k = {}, {}
for _, e in ipairs(bp_es) do if e.name == 'mlc'
then bp_mlcs[e.position.x..'_'..e.position.y] = e end end
if not next(bp_mlcs) then return bp_mlc_uids end -- no mlcs in blueprint
for _, e in ipairs(map_es) do
if not (e.valid and ( e.name == 'mlc'
or (e.name == 'entity-ghost' and e.ghost_name == 'mlc') ))
then goto skip end
k = e.position.x..'_'..e.position.y
be, bp_mlcs[k] = bp_mlcs[k]
if not be or e.name == 'entity-ghost' then goto skip end -- ghosts have tags already
bp_mlc_uids[be.entity_number] = e.unit_number
::skip:: end
if next(bp_mlcs) then return end -- blueprint entities left unmapped
return bp_mlc_uids
end
local function blueprint_map_validate(bp_es, bp_map)
-- Blueprint ev.mapping can be invalidated by other mods acting on this event, so checked first
-- See https://forums.factorio.com/viewtopic.php?p=457054#p457054 for more details
local bp_check, bp_mlc_uids = {}, {}
for _, e in ipairs(bp_es) do bp_check[e.entity_number] = e.name end
for bp_idx, e in pairs(bp_map) do
if not e.valid or bp_check[bp_idx] ~= e.name then return end -- abort on mismatch
if e.name == 'mlc' then bp_mlc_uids[bp_idx] = e.unit_number end
bp_check[bp_idx] = nil
end
if next(bp_check) then return end -- not all bp entities are in the mapping
return bp_mlc_uids
end
local function on_setup_blueprint(ev)
local p = game.players[ev.player_index]
if not (p and p.valid) then return end
local bp = p.blueprint_to_setup
if not (bp and bp.valid_for_read) then bp = p.cursor_stack end
if not (bp and bp.valid_for_read and bp.is_blueprint)
then return console_warn( p, 'BUG: Failed to detect blueprint'..
' item/info, Moon Logic Combinator code (if any) WILL NOT be stored there' ) end
local bp_es = bp.get_blueprint_entities()
if not bp_es then return end -- tiles-only blueprint, no mlcs
local bp_map = ev.mapping.valid and ev.mapping.get() or {}
local bp_mlc_uids = blueprint_map_validate(bp_es, bp_map) -- try using ev.mapping first
if not bp_mlc_uids then -- fallback - map entities via blueprint_match_position
-- Entity name filters are not used because both ghost/real entities must be matched
local map_es = p.surface.find_entities(ev.area)
bp_mlc_uids = blueprint_match_positions(bp_es, map_es)
end
if not bp_mlc_uids then return console_warn( p, 'BUG: Failed to match blueprint'..
' entities to ones on the map, combinator settings WILL NOT be stored in this blueprint!' ) end
for bp_idx, uid in pairs(bp_mlc_uids)
do bp.set_blueprint_entity_tag(bp_idx, 'mlc_code', storage.combinators[uid].code) end
end
script.on_event(defines.events.on_player_setup_blueprint, on_setup_blueprint)
local function on_built(ev)
local e = ev.created_entity or ev.entity -- latter for revive event
if not e.valid then return end
local mlc = out_wire_connect_mlc{e=e}
storage.combinators[e.unit_number] = mlc
-- Blueprints - try to restore settings from tags stored there on setup,
-- or fallback to old method with uid stored in a constant for simple copy-paste if tags fail
if ev.tags and ev.tags.mlc_code then mlc.code = ev.tags.mlc_code
else
local ecc_params = e.get_or_create_control_behavior().parameters
local uid_src = ecc_params.first_constant or 0
if uid_src < 0 then uid_src = uid_src + 0x100000000 end -- int -> uint conversion
if uid_src ~= 0 then
local mlc_src = storage.combinators[uid_src]
if mlc_src then mlc.code = mlc_src.code else
mlc.code = ('-- No code was stored in blueprint and'..
' Moon Logic [%s] is unavailable for OTA code update'):format(uid_src) end
end end
end
script.on_event(defines.events.on_built_entity, on_built, mlc_filter)
script.on_event(defines.events.on_robot_built_entity, on_built, mlc_filter)
script.on_event(defines.events.on_space_platform_built_entity, on_built, mlc_filter)
script.on_event(defines.events.script_raised_built, on_built, mlc_filter)
script.on_event(defines.events.script_raised_revive, on_built, mlc_filter)
local function on_entity_copy(ev)
if ev.destination.name == 'mlc-core' then return ev.destination.destroy() end -- for clone event
if not (ev.source.name == 'mlc' and ev.destination.name == 'mlc') then return end
local uid_src, uid_dst = ev.source.unit_number, ev.destination.unit_number
local mlc_old_outs = storage.combinators[uid_dst]
mlc_remove(uid_dst, true)
if mlc_old_outs
then mlc_old_outs = {mlc_old_outs.out_red, mlc_old_outs.out_green}
-- For cloned entities, mlc-core's might not yet exist - create/register them here, remove clones above
-- It'd give zero-outputs for one tick, but probably not an issue, easier to handle it like this
else mlc_old_outs = {out_wire_connect_both(ev.destination)} end
storage.combinators[uid_dst] = tdc(storage.combinators[uid_src])
local mlc_dst, mlc_src = storage.combinators[uid_dst], storage.combinators[uid_src]
mlc_dst.e, mlc_dst.next_tick, mlc_dst.core = ev.destination, 0
mlc_dst.out_red, mlc_dst.out_green = table.unpack(mlc_old_outs)
guis.history_insert(mlc_src, mlc_src.code or '', storage.guis[uid_dst])
end
script.on_event(
defines.events.on_entity_cloned, on_entity_copy, -- can be tested via clone in /editor
{{filter='name', name='mlc'}, {filter='name', name='mlc-core'}} )
script.on_event(defines.events.on_entity_settings_pasted, on_entity_copy)
local function on_destroyed(ev) mlc_remove(ev.entity.unit_number) end
local function on_mined(ev) mlc_remove(ev.entity.unit_number, nil, true) end
script.on_event(defines.events.on_pre_player_mined_item, on_mined, mlc_filter)
script.on_event(defines.events.on_robot_pre_mined, on_mined, mlc_filter)
script.on_event(defines.events.on_entity_died, on_destroyed, mlc_filter)
script.on_event(defines.events.script_raised_destroy, on_destroyed, mlc_filter)
-- ----- on_tick handling - lua code, gui updates -----
local function format_mlc_err_msg(mlc)
if not (mlc.err_parse or mlc.err_run or mlc.err_out) then return end
local err_msg = ''
for prefix, err in pairs{ ParserError=mlc.err_parse,
RuntimeError=mlc.err_run, OutputError=mlc.err_out } do
if not err then goto skip end
if err_msg ~= '' then err_msg = err_msg..' :: ' end
err_msg = err_msg..('%s: %s'):format(prefix, err)
::skip:: end
return err_msg
end
local function signal_icon_tag(sig)
local sig = storage.signals[sig]
if not sig then return '' end
if sig.type == 'virtual' then return '[virtual-signal='..sig.name..'] ' end
if (sig.type == nil)then return "" end
if helpers.is_valid_sprite_path(sig.type..'/'..sig.name)
then return '[img='..sig.type..'/'..sig.name..'] ' end
end
local function quality_icon_tag(qname)
if not qname then return '' end
if helpers.is_valid_sprite_path('quality/'..qname)
then return '[img=quality/'..qname..']' end
end
local function update_signals_in_guis()
local gui_flow, label, mlc, cb, sig, mlc_out, mlc_out_idx, mlc_out_err
local colors = {red={1,0.3,0.3}, green={0.3,1,0.3}}
for uid, gui_t in pairs(storage.guis) do
mlc = storage.combinators[uid]
if not (mlc and mlc.e.valid) then mlc_remove(uid); goto skip end
gui_flow = gui_t.signal_pane
if not (gui_flow and gui_flow.valid) then goto skip end
gui_flow.clear()
-- Inputs
for k, color in pairs(colors) do
cb = cn_wire_signals(mlc.e, defines.wire_type[k])
for sig, v in pairs(cb) do
if v == 0 then goto skip end
if not sig then goto skip end
local signame, qname = cn_sig_quality(sig)
local icon = signal_icon_tag(cn_sig_str(signame))
if qname then
icon = quality_icon_tag(qname) .. icon
end
label = gui_flow.add{
type='label', name='mlc-sig-in-'..k..'-'..sig,
caption=('[%s] %s%s = %s'):format(
conf.get_wire_label(k), icon, sig, v ) }
label.style.font_color = color
label.tags = {signal=sig}
::skip:: end end
-- Outputs
mlc_out, mlc_out_idx, mlc_out_err = {}, {}, tc((Combinators[uid] or {})._out or {})
for k, cb in pairs{red=mlc.out_red, green=mlc.out_green} do
cb = cb.get_control_behavior()
for _, cbs in pairs(cb.sections[1].filters or {}) do
sig, label = cbs.value.name, conf.get_wire_label(k)
if not sig then goto cb_slots_end end
if cbs.value.quality ~= nil and cbs.value.quality ~= "normal" then
sig = cbs.value.quality.."/"..sig
end
mlc_out_err[sig],
mlc_out_err[('%s/%s'):format(k, sig)],
mlc_out_err[('%s/%s'):format(label, sig)] = nil
sig = cn_sig_str(cbs.value)
mlc_out_err[sig],
mlc_out_err[('%s/%s'):format(k, sig)],
mlc_out_err[('%s/%s'):format(label, sig)] = nil
if cbs.min ~= 0 then
if not mlc_out[sig] then mlc_out_idx[#mlc_out_idx+1], mlc_out[sig] = sig, {} end
mlc_out[sig][k] = cbs.min end
end ::cb_slots_end:: end
table.sort(mlc_out_idx)
for val, k in pairs(mlc_out_idx) do
local signame, qname = cn_sig_quality(k)
val, sig, label = mlc_out[k], storage.signals[signame].name, signal_icon_tag(signame)
if string.sub(signame,1,1)== '~' then
sig = "~"..sig
end
if qname then
label = quality_icon_tag(qname) .. label
sig = qname.."/"..sig
end
if val['red'] == val['green'] then
k = gui_flow.add{ type='label', name='mlc-sig-out-'..sig,
caption=('[out] %s%s = %s'):format(label, sig, val['red'] or 0) }
k.tags = {signal=sig}
else for k, color in pairs(colors) do
k = gui_flow.add{ type='label', name='mlc-sig-out/'..k..'-'..sig,
caption=('[out/%s] %s%s = %s'):format(conf.get_wire_label(k), label, sig, val[k] or 0) }
k.style.font_color = color
k.tags = {signal=sig}
end end end
-- Remaining invalid signals and errors
local n = 0 -- to dedup bogus non-string signal keys that have same string repr
for sig, val in pairs(mlc_out_err) do
cb, val = pcall(serpent.line, val, {compact=true, nohuge=false})
if not cb then val = '<err>' end
if val:len() > 8 then val = val:sub(1, 8)..'+' end
gui_flow.add{ type='label', name=('mlc-sig-out/err-%d-%s'):format(n, sig),
caption=('[color=#ce9f7f][out-invalid] %s = %s[/color]'):format(sig, val) }
n = n + 1
end
gui_t.mlc_errors.caption = format_mlc_err_msg(mlc) or ''
::skip:: end
end
local function alert_about_mlc_error(mlc_env, err_msg)
local p = mlc_env._e.last_user
if p.valid and p.connected
then p = {p} else p = p.force.connected_players end
mlc_env._alert = p
for _, p in ipairs(p) do
p.add_custom_alert( mlc_env._e, mlc_err_sig,
'Moon Logic Error ['..mlc_env._uid..']: '..err_msg, true )
end
end
local function alert_clear(mlc_env)
local p = mlc_env._alert or {}
for _, p in ipairs(p) do
if p.valid and p.connected then p.remove_alert{icon=mlc_err_sig} end
end
mlc_env._alert = nil
end
local function run_moon_logic_tick(mlc, mlc_env, tick)
-- Runs logic of the specified combinator, reading its input and setting outputs
local out_tick, out_diff = mlc.next_tick, tc(mlc_env._out)
local dbg = mlc.vars.debug and function(fmt, ...)
mlc_log((' -- moon-logic [%s]: %s'):format(mlc_env._uid, fmt:format(...))) end
mlc.vars.delay, mlc.vars.var, mlc.vars.debug, mlc.vars.irq, mlc.irq = 1, mlc.vars.var or {}
if mlc.e.energy < conf.energy_fail_level then
mlc.state = 'no-power'
mlc_update_led(mlc, mlc_env)
mlc.next_tick = game.tick + conf.energy_fail_delay
return
end
if dbg then -- debug
dbg('--- debug-run start [tick=%s] ---', tick)
mlc_env.debug_wires_set({})
dbg('env-before :: %s', serpent.line(mlc.vars))
dbg('out-before :: %s', serpent.line(mlc_env._out)) end
mlc_env._out['mlc-error'] = nil -- for internal use
do
local st, err = pcall(mlc_env._func)
if not st then mlc.err_run = err or '[unspecified lua error]'
else
mlc.state, mlc.err_run = 'run'
if mlc_env._out['mlc-error'] ~= 0 then -- can be used to stop combinator
mlc.err_run = 'Internal mlc-error signal set'
mlc_env._out['mlc-error'] = nil -- signal will be emitted via mlc.state
end
end
end
if dbg then -- debug
dbg('used-inputs :: %s', serpent.line(mlc_env.debug_wires_set()))
dbg('env-after :: %s', serpent.line(mlc.vars))
dbg('out-after :: %s', serpent.line(mlc_env._out)) end
local delay = tonumber(mlc.vars.delay) or 1
if delay > conf.led_sleep_min then mlc.state = 'sleep' end
mlc.next_tick = tick + delay
local sig = mlc.vars.irq
if sig then
sig = cn_sig(sig)
if sig then mlc.irq, mlc.irq_delay = sig, tonumber(mlc.vars.irq_min_interval) else
mlc.err_run = ('Unknown/ambiguous "irq" signal: %s'):format(serpent.line(mlc.vars.irq)) end
end
for sig, v in pairs(mlc_env._out) do
if out_diff[sig] ~= v then out_diff[sig] = v
else out_diff[sig] = nil end
end
local out_sync = next(out_diff) or out_tick == 0 -- force sync after reset
if dbg then -- debug
for sig, v in pairs(out_diff) do
if not mlc_env._out[sig] then out_diff[sig] = '-'
end end
dbg('out-sync=%s out-diff :: %s', out_sync and true, serpent.line(out_diff)) end
if out_sync then mlc_update_output(mlc, mlc_env._out) end
local err_msg = format_mlc_err_msg(mlc)
if err_msg then
mlc.state = 'error'
if dbg then dbg('error :: %s', err_msg) end -- debug
alert_about_mlc_error(mlc_env, err_msg)
end
if dbg then dbg('--- debug-run end [tick=%s] ---', tick) end -- debug
mlc_update_led(mlc, mlc_env)
if mlc.vars.ota_update_from_uid then
local mlc_src = mlc.vars.ota_update_from_uid
mlc_src = mlc_src ~= mlc_env._uid and
storage.combinators[mlc.vars.ota_update_from_uid]
if mlc_src and mlc_src.code ~= mlc.code
then guis.save_code(mlc_env._uid, mlc_src.code) end
mlc.vars.ota_update_from_uid = nil
end
end
local function on_tick(ev)
local tick = ev.tick
if sandbox_env_base.game then sandbox_env_base.game.tick = tick end
for uid, mlc in pairs(storage.combinators) do
local mlc_env = Combinators[uid]
if not mlc_env then mlc_env = mlc_init(mlc.e) end
if not ( mlc_env and mlc.e.valid
and mlc.out_red.valid and mlc.out_green.valid )
then mlc_remove(uid); goto skip end
local err_msg = format_mlc_err_msg(mlc)
if err_msg then
if tick % conf.logic_alert_interval == 0
then alert_about_mlc_error(mlc_env, err_msg) end
goto skip -- suspend combinator logic until errors are addressed
elseif mlc_env._alert then alert_clear(mlc_env) end
if mlc.irq and (mlc.irq_tick or 0) < tick - (mlc.irq_delay or 0)
and mlc.e.get_signal(mlc.irq, defines.wire_connector_id.combinator_input_green, defines.wire_connector_id.combinator_input_red) ~= 0
then mlc.irq_tick, mlc.next_tick = tick end
if tick >= (mlc.next_tick or 0) and mlc_env._func then
run_moon_logic_tick(mlc, mlc_env, tick)
for _, p in ipairs(game.connected_players)
do guis.vars_window_update(p.index, uid) end
if mlc.err_run then guis.update_error_highlight(uid, mlc, mlc.err_run) end
end
::skip:: end
if next(storage.guis)
and game.tick % conf.gui_signals_update_interval == 0
then update_signals_in_guis() end
end
script.on_event(defines.events.on_tick, on_tick)
-- ----- GUI events and entity interactions -----
function load_code_from_gui(code, uid) -- note: in global _ENV, used from gui.lua
local mlc, mlc_env = storage.combinators[uid], Combinators[uid]
if not ( mlc and mlc.e.valid
and mlc.out_red.valid and mlc.out_green.valid )
then return mlc_remove(uid) end
mlc.code = code or ''
if not mlc_env then return mlc_init(mlc.e) end
mlc_update_code(mlc, mlc_env)
if not mlc.err_parse then
for _, player in pairs(game.players)
do player.remove_alert{entity=mlc_env._e} end
else guis.update_error_highlight(uid, mlc, mlc.err_parse) end
end
function clear_outputs_from_gui(uid) -- note: in global _ENV, used from gui.lua
local mlc, mlc_env = storage.combinators[uid], Combinators[uid]
if not (mlc and mlc_env) then return end
cn_output_table_replace(mlc_env._out)
mlc_update_output(mlc, mlc_env._out)
end
script.on_event(defines.events.on_gui_opened, function(ev)
if not ev.entity then return end
local player = game.players[ev.player_index]
local e = player.opened
if not (e and e.name == 'mlc') then return end
if not storage.combinators[e.unit_number] then
player.opened = nil
return console_warn(player, 'BUG: Combinator #'..e.unit_number..' is not registered with mod code')
end
local gui_t = storage.guis[e.unit_number]
if not gui_t then guis.open(player, e)
elseif player.index == gui_t.mlc_gui.player_index then
-- This can happen when clicking same mlc again with code box focused
-- "return" here will open regular combinator gui, so do something else
-- Not sure how to handle this better - setting anything to player.opened closes stuff
local code = gui_t.mlc_code.text
gui_t = guis.open(player, e)
gui_t.mlc_code.text = code -- restore currently-edited code
else
e = game.players[gui_t.mlc_gui.player_index or 0]
e = e and e.name or 'Another player'
player.print(e..' already opened this combinator', {1,1,0})
end
end)
script.on_event(defines.events.on_gui_click, guis.on_gui_click)
script.on_event(defines.events.on_gui_closed, guis.on_gui_close)
if conf.code_history_enabled -- this adds a lot of unpleasant lag to editing text in that textbox
then script.on_event(defines.events.on_gui_text_changed, guis.on_gui_text_changed) end
-- ----- Keyboard editing hotkeys -----
-- Most editing hotkeys only work if one window is opened,
-- as I don't know how to check which one is focused otherwise.
-- Keybindings don't work in general when text-box element is focused.
local function get_active_gui()
local uid, gui_t
for uid_chk, gui_t_chk in pairs(storage.guis) do
if not uid
then uid, gui_t = uid_chk, gui_t_chk
else uid, gui_t = nil; break end
end
return uid, gui_t
end
script.on_event('mlc-code-save', function(ev)
local uid, gui_t = get_active_gui()
if uid then guis.save_code(uid) end
end)
script.on_event('mlc-code-undo', function(ev)
local uid, gui_t = get_active_gui()
if not gui_t then return end
local mlc = storage.combinators[gui_t.uid]
if mlc then guis.history_restore(gui_t, mlc, -1) end
end)
script.on_event('mlc-code-redo', function(ev)
local uid, gui_t = get_active_gui()