forked from Factorio-Access/FactorioAccess
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfa-info.lua
More file actions
2087 lines (1871 loc) · 77.9 KB
/
Copy pathfa-info.lua
File metadata and controls
2087 lines (1871 loc) · 77.9 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
--[[
Announcement of information.
This file is used to build up the strings used for entity statuses and cursor
movement, which are honestlyh most of the mod's "magic". It consists of
functions which either produce strings about entities or produce joined up
strings to send to the player. Since the applicability conditions for each
announcement are complex and we would prefer not to centralize them, we do this
by passing around a MessageBuilder and some context as to what's going on.
The localisation is in entity-info.cfg. We do not distinguish between cursor
level information and status level information in the localisation, because it
is not necessarily the case that things fall into one or the other, or that we
won't change our mind later or maybe even go as far as adding settings for this
stuff.
]]
local dirs = defines.direction
local util = require("util")
local Filters = require("scripts.filters")
local UiRouter = require("scripts.ui.router")
local BuildingTools = require("scripts.building-tools")
local Circuits = require("scripts.circuit-networks")
local Consts = require("scripts.consts")
local Driving = require("scripts.driving")
local Electrical = require("scripts.electrical")
local Equipment = require("scripts.equipment")
local FaUtils = require("scripts.fa-utils")
local F = require("scripts.field-ref")
local Fluids = require("scripts.fluids")
local Geometry = require("scripts.geometry")
local Graphics = require("scripts.graphics")
local Heat = require("scripts.heat")
local Localising = require("scripts.localising")
local MessageBuilder = require("scripts.message-builder")
local NetworkShape = require("scripts.network-shape")
local Rails = require("scripts.rails")
local ResourceMining = require("scripts.resource-mining")
local TH = require("scripts.table-helpers")
local Trains = require("scripts.trains")
local TransportBelts = require("scripts.transport-belts")
local Viewpoint = require("scripts.viewpoint")
local Wires = require("scripts.wires")
local BotLogistics = require("scripts.worker-robots")
local mod = {}
---@class fa.Info.EntInfoContext
---@field message fa.MessageBuilder
---@field is_scanner boolean
---@field ent LuaEntity
---@field pindex number
---@field player LuaPlayer
---@field cursor_pos fa.Point Not necessarily the player's actual cursor.
---@class fa.Info.EntStatusContext
---@field message fa.MessageBuilder
---@field ent LuaEntity
---@field pindex number
---@field player LuaPlayer
---@field power_rate number
---@field drain number
---@field uses_energy boolean
-- Present a list like iron plate x1, transport belt legendary x2, ...
---@param list ({ name: string|LuaItemPrototype, quality: string|LuaQualityPrototype|nil, count: number})[]
---@param truncate number?
---@param protos table<string, LuaItemPrototype | LuaFluidPrototype>?
local function present_list(list, truncate, protos)
local contents = TH.rollup2(list, F.name().get, function(i)
return i.quality or "normal"
end, F.count().get)
-- Now that everything is together we must unroll it again, then sort.
---@type ({ count: number, name: string, quality: LuaQualityPrototype })[]
local final = {}
for name, quals in pairs(contents) do
for qual, count in pairs(quals) do
table.insert(final, { count = count, name = name, quality = prototypes.quality[qual] })
end
end
-- Careful: this is actually a reverse sort.
table.sort(final, function(a, b)
if a.count == b.count and a.name == b.name then
return a.quality.level > b.quality.level
elseif a.count == b.count then
return a.name > b.name
else
return a.count > b.count
end
end)
local endpoint = #final
local extra = false
if truncate then
extra = truncate < endpoint
endpoint = math.min(endpoint, truncate)
end
if not next(final) then return { "fa.ent-info-inventory-empty" } end
local entries = {}
for i = 1, endpoint do
local e = final[i]
table.insert(
entries,
Localising.localise_item_or_fluid(
{ name = e.name, quality = e.quality, count = e.count },
protos or prototypes.item
)
)
end
if extra then
table.insert(entries, Localising.localise_item({ name = Localising.ITEM_OTHER, count = #final - truncate }))
end
local joined = FaUtils.localise_cat_table(entries, ", ")
return { "fa.ent-info-inventory-presentation", joined }
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_facing(ctx)
local ent = ctx.ent
local effective_direction
-- Set in the case where we detect symmetry.
local secondary_effective_direction
if
(ent.prototype.is_building and ent.supports_direction)
or (ent.type == "entity-ghost" and ent.ghost_prototype.is_building and ent.ghost_prototype.supports_direction)
then
effective_direction = FaUtils.direction_lookup(ent.direction)
if ent.type == "generator" then
--For steam engines and steam turbines, north = south and east = west
secondary_effective_direction = FaUtils.direction_lookup(FaUtils.rotate_180(ent.direction))
end
elseif ent.type == "locomotive" or ent.type == "car" then
effective_direction = (FaUtils.get_heading_info(ent))
end
if effective_direction and secondary_effective_direction then
ctx.message:fragment({ "fa.ent-info-facing-symmetric", effective_direction, secondary_effective_direction })
elseif effective_direction then
ctx.message:fragment({ "fa.ent-info-facing", effective_direction })
end
end
-- Announces if the entity is marked for upgrading or deconstruction. Folded
-- into one function, as these are mutually exclusive states as far as we know.
---@param ctx fa.Info.EntInfoContext
local function ent_info_marked_for_upgrade_deconstruct(ctx)
if ctx.ent.to_be_deconstructed() then
ctx.message:fragment({ "fa.ent-info-marked-for-deconstruction" })
elseif ctx.ent.to_be_upgraded() then
ctx.message:fragment({ "fa.ent-info-marked-for-upgrading" })
end
-- Otherwise it is not marked.
end
-- If this entity generates electricity, tell the player how much.
---@param ctx fa.Info.EntInfoContext
local function ent_info_power_production(ctx)
local ent = ctx.ent
if ctx.ent.prototype.type == "generator" then
local power1 = ent.energy_generated_last_tick * 60
local power2 = ent.prototype.get_max_energy_production(ent.quality) * 60
local power_load_pct = math.ceil(power1 / power2 * 100)
if power2 ~= nil then
ctx.message:fragment({ "fa.ent-info-generator-load", power_load_pct }):fragment({
"fa.ent-info-generator-production",
Electrical.get_power_string(power1),
Electrical.get_power_string(power2),
})
else
ctx.message:fragment({ "fa.ent-info-generator-production", Electrical.get_power_string(power1) })
end
end
end
-- If the entity has a status which is super important, for example no power or
-- output full, tell the player. These are things that we judge to be important
-- enough that checking status shouldn't be required.
---@param ctx fa.Info.EntInfoContext
local function ent_info_important_statuses(ctx)
local ent = ctx.ent
local status = ent.status
local stat = defines.entity_status
if status ~= nil and status ~= stat.normal and status ~= stat.working then
if
status == stat.no_ingredients
or status == stat.no_input_fluid
or status == stat.no_minable_resources
or status == stat.item_ingredient_shortage
or status == stat.missing_required_fluid
or status == stat.no_ammo
then
ctx.message:fragment({ "fa.ent-info-input-missing" })
elseif status == stat.full_output or status == stat.full_burnt_result_output then
ctx.message:fragment({ "fa.ent-info-output-full" })
elseif status == defines.entity_status.pipeline_overextended then
ctx.message:fragment({ "entity-status.pipeline-overextended" })
end
end
end
-- "not connected to power" etc.
---@param ctx fa.Info.EntInfoContext
local function ent_info_power_status(ctx)
local ent = ctx.ent
if ent.prototype.electric_energy_source_prototype ~= nil and ent.is_connected_to_electric_network() == false then
ctx.message:fragment({ "fa.ent-info-no-power-connection" })
elseif ent.prototype.electric_energy_source_prototype ~= nil and ent.energy == 0 and ent.type ~= "solar-panel" then
ctx.message:fragment({ "fa.ent-info-no-power-empty-electric-network" })
end
end
-- Announces if the entity is a wall and a point at which the player may connect
-- the circuit network to control a gate.
---@param ctx fa.Info.EntInfoContext
local function ent_info_gate_connection_point(ctx)
if ctx.ent.type == "wall" and ctx.ent.get_control_behavior() ~= nil then
ctx.message:fragment({ "fa.ent-info-gate-circuit-network-connection" })
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_accumulator(ctx)
local ent = ctx.ent
if ent.type == "accumulator" then
local level = math.ceil(ent.energy / ent.electric_buffer_size * 100) --In percentage
local charge = math.ceil(ent.energy)
ctx.message:fragment({ "fa.ent-info-accumulator-charge", level, Electrical.get_power_string(charge) })
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_solar(ctx)
local ent = ctx.ent
if ent.type == "solar-panel" then
local s_time = ent.surface.daytime * 24 --We observed 18 = peak solar start, 6 = peak solar end, 11 = night start, 13 = night end
local solar_status = ""
if s_time > 13 and s_time <= 18 then
ctx.message:fragment({ "fa.ent-info-solar-increasing" })
elseif s_time > 18 or s_time < 6 then
ctx.message:fragment({ "fa.ent-info-solar-full-production" })
elseif s_time > 6 and s_time <= 11 then
ctx.message:fragment({ "fa.ent-info-solar-evening" })
elseif s_time > 11 and s_time <= 13 then
ctx.message:fragment({ "fa.ent-info-solar-night" })
end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_rocket_silo(ctx)
local ent = ctx.ent
if ent.name == "rocket-silo" then
if ent.rocket_parts ~= nil and ent.rocket_parts < 100 then
ctx.message:fragment({ "fa.ent-info-silo-partial", ent.rocket_parts })
elseif ent.rocket_parts ~= nil then
ctx.message:fragment({ "fa.ent-info-silo-complete" })
end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_beacon_status(ctx)
local ent = ctx.ent
if ent.name == "beacon" then
local modules = ent.get_module_inventory()
if not modules then return end
local presenting = present_list(modules.get_contents())
if presenting then ctx.message:fragment(presenting) end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_constant_combinator(ctx)
local ent = ctx.ent
if ent.type == "constant-combinator" then
ctx.message:fragment(Circuits.constant_combinator_signals_info(ent, ctx.pindex))
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_resource(ctx)
local ent = ctx.ent
if ent.type == "resource" then
if not ent.initial_amount then
-- initial_amount is nil for non-infinite resources.
ctx.message:fragment({ "fa.ent-info-resource-noninfinite", ent.amount })
else
-- The game computes it this way then displays it as 403% or w/e.
local percentage = ent.prototype.normal_resource_amount / 100
ctx.message:fragment({ "fa.ent-info-resource-infinite", percentage })
end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_rail(ctx)
local ent = ctx.ent
-- TODO: really we shouldn't need pindex here, but for now rails aren't
-- localised properly.
if ent.name == "straight-rail" or ent.name == "curved-rail" then return Rails.rail_ent_info(ctx.pindex, ent) end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_character(ctx)
local ent = ctx.ent
if ent.name == "character" then
local p = ent.player
local p2 = ent.associated_player
if p ~= nil and p.valid and p.name ~= nil and p.name ~= "" then
ctx.message:fragment(p.name)
elseif p2 ~= nil and p2.valid and p2.name ~= nil and p2.name ~= "" then
ctx.message:fragment(p2.name)
elseif p ~= nil and p.valid and p.index == ctx.pindex then
ctx.message:fragment({ "fa.ent-info-self-character" })
elseif ctx.pindex ~= nil then
ctx.message:fragment(tostring(ctx.pindex))
end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_character_corpse(ctx)
local ent = ctx.ent
if ent.name == "character-corpse" then
if ent.character_corpse_player_index == ctx.pindex then
ctx.message:fragment({ "fa.ent-info-corpse-is-self" })
elseif ent.character_corpse_player_index ~= nil then
ctx.message:fragment({ "fa.ent-info-corpse-of-other" })
end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_container(ctx)
local ent = ctx.ent
if ent.type == "container" or ent.type == "logistic-container" or ent.type == "infinity-container" then
--Chests etc: Report the most common item and say "and other items" if there are other types.
local inv = ent.get_inventory(defines.inventory.chest)
assert(inv)
local presenting = present_list(inv.get_contents(), 3)
if presenting then ctx.message:fragment(presenting) end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_fluid_contents(ctx)
-- Crafting machines are special, and this is folded into their readiness
-- status.
if Consts.CRAFTING_MACHINES[ctx.ent.type] then return end
-- If it can't hold fluids, no point.
if #ctx.ent.fluidbox == 0 then return end
local fluids = ctx.ent.get_fluid_contents()
if not next(fluids) then
ctx.message:fragment({ "fa.ent-info-inventory-empty" })
return
end
local unrolled = {}
for f, c in pairs(fluids) do
table.insert(unrolled, { name = f, count = c })
end
ctx.message:fragment(present_list(unrolled, nil, prototypes.fluid))
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_logistic_network(ctx)
local ent = ctx.ent
-- very unclear: isn't this just entity.logistic_network? To revisit after
-- this file is refactored.
if ent.type == "logistic-container" then
local network = ent.surface.find_logistic_network_by_position(ent.position, ent.force)
if network == nil then
local nearest_roboport = FaUtils.find_nearest_roboport(ent.surface, ent.position, 5000)
if nearest_roboport == nil then
ctx.message:fragment({ "fa.ent-info-logistic-not-in-network", 5000 })
else
local dist = math.ceil(util.distance(ent.position, nearest_roboport.position) - 25)
local dir = FaUtils.direction_lookup(FaUtils.get_direction_biased(nearest_roboport.position, ent.position))
ctx.message:fragment({
"fa.ent-info-logistic-not-in-network-with-near",
nearest_roboport.backer_name,
dist,
dir,
})
end
else
local network_name = network.cells[1].owner.backer_name
ctx.message:fragment({ "fa.ent-info-logistic-in-network", network_name })
end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_infinity_pipe(ctx)
local ent = ctx.ent
if ent.name == "infinity-pipe" then
local filter = ent.get_infinity_pipe_filter()
if filter == nil then
ctx.message:fragment({ "fa.ent-info-infinity-pipe-draining" })
else
ctx.message:fragment({ "fa.ent-info-infinity-pipe-producing", filter.name })
end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_belt_shape(ctx)
local e = ctx.ent
local t = e.type
-- Only belts and underground belts can have shapes, at least for right now.
if t ~= "transport-belt" and t ~= "underground-belt" then return end
local node = TransportBelts.Node.create(e)
local shape_info = node:get_shape_info()
if shape_info.corner then
local key
local dir
if shape_info.corner == TransportBelts.CORNER_KINDS.LEFT then
key = "fa.ent-info-belt-shape-left"
-- we say "from the", so we aren't undoing it and counterclockwise for
-- left is right.
dir = Geometry.dir_counterclockwise_90(e.direction)
elseif shape_info.corner == TransportBelts.CORNER_KINDS.RIGHT then
key = "fa.ent-info-belt-shape-right"
dir = Geometry.dir_clockwise_90(e.direction)
end
ctx.message:fragment({ key, FaUtils.direction_lookup(dir) })
end
-- Sideloads: none, left, right, or both.
if not shape_info.merge and (shape_info.left_sideload or shape_info.right_sideload) then
if not shape_info.right_sideload then
-- No right sideload, must be left.
ctx.message:fragment({ "fa.ent-info-belt-shape-left-sideload" })
elseif not shape_info.left_sideload then
ctx.message:fragment({ "fa.ent-info-belt-shape-right-sideload" })
else
ctx.message:fragment({ "fa.ent-info-belt-shape-double-sideload" })
end
elseif shape_info.merge then
ctx.message:fragment({ "fa.ent-info-belt-shape-merge" })
end
-- First the "primary shape" if you will: corners, pouring, etc.
if not shape_info.has_input and not shape_info.has_output then
ctx.message:fragment({ "fa.ent-info-belt-shape-unit" })
elseif shape_info.is_pouring then
ctx.message:fragment({ "fa.ent-info-belt-shape-pouring" })
elseif shape_info.has_input and not shape_info.has_output then
ctx.message:fragment({ "fa.ent-info-belt-shape-stop" })
elseif shape_info.has_output and not shape_info.has_input then
ctx.message:fragment({ "fa.ent-info-belt-shape-start" })
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_pipe_shape(ctx)
if ctx.ent.type == "pipe" then
local shape_info = Fluids.get_pipe_shape(ctx.ent)
local s, d = shape_info.shape, shape_info.direction
local d_str = FaUtils.direction_lookup(d)
local conns = ctx.ent.fluidbox.get_pipe_connections(1)
local conn_count = 0
for _, c in pairs(conns) do
if c.target then conn_count = conn_count + 1 end
end
-- We must be careful. Pipe shapes do not account for other kinds of connection, so we must compare with the
-- expected count as well. Otherwise we will say that a pipe is both connected and not connected at the same time.
-- This is just a boring if table which appends fragments. no special logic here.
if s == NetworkShape.SHAPE.END and conn_count == 1 then
ctx.message:fragment({ "fa.ent-info-pipe-end", FaUtils.direction_lookup(FaUtils.rotate_180(d)) })
elseif s == NetworkShape.SHAPE.ALONE and conn_count == 0 then
ctx.message:fragment({ "fa.ent-info-pipe-alone" })
elseif s == NetworkShape.SHAPE.STRAIGHT and conn_count == 2 then
local key = d == defines.direction.north and "fa.ent-info-pipe-vertical" or "fa.ent-info-pipe-horizontal"
ctx.message:fragment({ key })
elseif s == NetworkShape.SHAPE.CORNER and conn_count == 2 then
local c1, c2
if d == defines.direction.northwest then
c1 = defines.direction.south
c2 = defines.direction.east
elseif d == defines.direction.northeast then
c1 = defines.direction.south
c2 = defines.direction.west
elseif d == defines.direction.southwest then
c1 = defines.direction.north
c2 = defines.direction.east
elseif d == defines.direction.southeast then
c1 = defines.direction.north
c2 = defines.direction.west
else
error("unreachable! " .. serpent.line({ s = s, d = d }))
end
ctx.message:fragment({ "fa.ent-info-pipe-corner", FaUtils.direction_lookup(c1), FaUtils.direction_lookup(c2) })
elseif s == NetworkShape.SHAPE.CROSS and conn_count == 4 then
ctx.message:fragment({ "fa.ent-info-pipe-cross" })
elseif s == NetworkShape.SHAPE.T then
local key = "fa.ent-info-pipe-t-vertical"
if d == defines.direction.north or d == defines.direction.south then key = "fa.ent-info-pipe-t-horizontal" end
ctx.message:fragment({ key, FaUtils.direction_lookup(FaUtils.rotate_180(d)) })
end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_underground_belt_type(ctx)
local ent = ctx.ent
if ent.type == "underground-belt" then
if ent.belt_to_ground_type == "input" then
ctx.message:fragment({ "fa.ent-info-underground-belt-entrance" })
elseif ent.belt_to_ground_type == "output" then
ctx.message:fragment({ "fa.ent-info-underground-belt-exit" })
end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_train_stop(ctx)
local ent = ctx.ent
if ent.name == "train-stop" then
local limit = ent.trains_limit or 0
ctx.message:fragment({ "fa.ent-info-train-stop", ent.backer_name, limit })
end
end
-- Returns train name announcement with id fallback.
---@param ctx fa.Info.EntInfoContext
local function ent_info_train_owner(ctx)
local ent = ctx.ent
if ent.name == "locomotive" or ent.name == "cargo-wagon" or ent.name == "fluid-wagon" then
ctx.message:fragment({ "fa.ent-info-of-train", Trains.get_train_name(ent.train) })
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_rail_signal_state(ctx)
-- TODO: this should be folded into basic entity state where it belongs.
local ent = ctx.ent
if ent.name == "rail-signal" or ent.name == "rail-chain-signal" then
if ent.status == defines.entity_status.not_connected_to_rail then
ctx.message:fragment({ "fa.ent-info-rail-signal-not-connected" })
elseif ent.status == defines.entity_status.cant_divide_segments then
ctx.message:fragment({ "fa.ent-info-rail-signal-not-dividing" })
else
ctx.message:fragment(Rails.get_signal_state_info(ent))
end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_rail_signal_heading(ctx)
local ent = ctx.ent
if ent.name == "rail-signal" or ent.name == "rail-chain-signal" then
ctx.message:fragment({
"fa.ent-info-rail-signal-heading",
FaUtils.direction_lookup(FaUtils.rotate_180(ent.direction)),
})
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_temperature(ctx)
local ent = ctx.ent
if ent.temperature ~= nil then ctx.message:fragment({ "fa.ent-info-temperature", math.floor(ent.temperature) }) end
end
-- NOTE: pushes multiple list items.
---@param ctx fa.Info.EntInfoContext
local function ent_info_nuclear_neighbor_bonus(ctx)
local ent = ctx.ent
if ent.name == "nuclear-reactor" then
if ent.temperature > 900 then ctx.message:list_item({ "fa.ent-info-nuclear-reactor-explodes" }) end
if ent.energy > 0 then ctx.message:list_item({ "fa.ent-info-nuclear-reactor-consuming" }) end
ctx.message:list_item({ "fa.ent-info-nuclear-reactor-neighbor-bonus", math.floor(ent.neighbour_bonus * 100) })
end
end
-- Name of item for items on the ground.
---@param ctx fa.Info.EntInfoContext
local function ent_info_item_on_ground(ctx)
local ent = ctx.ent
if ent.name == "item-on-ground" then
ctx.message:fragment(Localising.get_localised_name_with_fallback(ent.stack.prototype))
end
end
local dir_names = {
[defines.direction.north] = "north",
[defines.direction.east] = "east",
[defines.direction.south] = "south",
[defines.direction.west] = "west",
}
---@param ctx fa.Info.EntInfoContext
local function ent_info_heat_pipe_shape(ctx)
if ctx.ent.type == "heat-pipe" then
local shape_info = Heat.get_pipe_shape(ctx.ent)
local s, d = shape_info.shape, shape_info.direction
local d_str = FaUtils.direction_lookup(d)
local conn_count = Heat.get_total_heat_connections(ctx.ent)
-- We must be careful. Pipe shapes do not account for other kinds of connection, so we must compare with the
-- expected count as well. Otherwise we will say that a pipe is both connected and not connected at the same time.
-- This is just a boring if table which appends fragments. no special logic here.
if s == NetworkShape.SHAPE.END and conn_count == 1 then
ctx.message:fragment({ "fa.ent-info-pipe-end", FaUtils.direction_lookup(FaUtils.rotate_180(d)) })
elseif s == NetworkShape.SHAPE.ALONE and conn_count == 0 then
ctx.message:fragment({ "fa.ent-info-pipe-alone" })
elseif s == NetworkShape.SHAPE.STRAIGHT and conn_count == 2 then
local key = d == defines.direction.north and "fa.ent-info-pipe-vertical" or "fa.ent-info-pipe-horizontal"
ctx.message:fragment({ key })
elseif s == NetworkShape.SHAPE.CORNER and conn_count == 2 then
local c1, c2
if d == defines.direction.northwest then
c1 = defines.direction.south
c2 = defines.direction.east
elseif d == defines.direction.northeast then
c1 = defines.direction.south
c2 = defines.direction.west
elseif d == defines.direction.southwest then
c1 = defines.direction.north
c2 = defines.direction.east
elseif d == defines.direction.southeast then
c1 = defines.direction.north
c2 = defines.direction.west
else
error("unreachable! " .. serpent.line({ s = s, d = d }))
end
ctx.message:fragment({ "fa.ent-info-pipe-corner", FaUtils.direction_lookup(c1), FaUtils.direction_lookup(c2) })
elseif s == NetworkShape.SHAPE.CROSS and conn_count == 4 then
ctx.message:fragment({ "fa.ent-info-pipe-cross" })
elseif s == NetworkShape.SHAPE.T then
local key = "fa.ent-info-pipe-t-vertical"
if d == defines.direction.north or d == defines.direction.south then key = "fa.ent-info-pipe-t-horizontal" end
ctx.message:fragment({ key, FaUtils.direction_lookup(FaUtils.rotate_180(d)) })
end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_render_heat_ports(ctx)
local ent = ctx.ent
if
not (
ent
and ent.valid
and ent.prototype.heat_buffer_prototype
and next(ent.prototype.heat_buffer_prototype.connections)
)
then
return
end
local connection_points = Heat.get_connection_points(ent)
local found_dirs = {}
for _, conn in ipairs(connection_points) do
-- Draw the port itself (blue)
rendering.draw_circle({
color = { 0.5, 0.5, 1.0 },
radius = 0.15,
width = 2,
target = conn.position,
surface = ent.surface,
time_to_live = 30,
})
-- Now, draw only the first connected neighbor per direction (green)
local neighbors = Heat.get_connected_neighbors(ent, conn, false)
for _, neighbor_info in ipairs(neighbors) do
local n_ent = neighbor_info.entity
local n_cp = neighbor_info.connection_point
local dir
if conn.direction then
dir = conn.direction
else
dir = FaUtils.get_direction_biased(n_cp.position, ent.position)
end
if not found_dirs[dir] then
found_dirs[dir] = true
rendering.draw_circle({
color = { 0.8, 1.0, 0.5 },
radius = 0.18,
width = 2,
target = n_cp.position,
surface = n_ent.surface,
time_to_live = 30,
})
end
end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_heat_neighbors(ctx)
local ent = ctx.ent
if
not (
ent
and ent.valid
and ent.prototype.heat_buffer_prototype
and next(ent.prototype.heat_buffer_prototype.connections)
)
then
return
end
local connection_points = Heat.get_connection_points(ent)
if not connection_points or #connection_points == 0 then return end
-- Gather only those ports the cursor is "on" (distance threshold)
local relevant_ports = {}
for _, conn in ipairs(connection_points) do
if FaUtils.distance(conn.position, ctx.cursor_pos) < 0.5 then table.insert(relevant_ports, conn) end
end
if #relevant_ports == 0 then return end
-- Gather directions of all relevant ports
local dirs = {}
for _, conn in ipairs(relevant_ports) do
local dir = conn.direction
if dir == nil then dir = FaUtils.get_direction_biased(conn.position, ent.position) end
table.insert(dirs, dir or 16)
end
-- Localize heat port directions, passing raw direction constants
local function localise_heat_ports(dirs)
local n = #dirs
if n == 0 then
return nil
elseif n == 1 then
return { "fa.ent-info-heat-port-1", FaUtils.direction_lookup(dirs[1]) }
elseif n == 2 then
return { "fa.ent-info-heat-port-2", FaUtils.direction_lookup(dirs[1]), FaUtils.direction_lookup(dirs[2]) }
elseif n == 3 then
return {
"fa.ent-info-heat-port-3",
FaUtils.direction_lookup(dirs[1]),
FaUtils.direction_lookup(dirs[2]),
FaUtils.direction_lookup(dirs[3]),
}
elseif n == 4 then
return {
"fa.ent-info-heat-port-4",
FaUtils.direction_lookup(dirs[1]),
FaUtils.direction_lookup(dirs[2]),
FaUtils.direction_lookup(dirs[3]),
FaUtils.direction_lookup(dirs[4]),
}
end
end
if ent.type ~= "heat-pipe" then ctx.message:fragment(localise_heat_ports(dirs)) end
-- Gather connections: name + raw direction for each neighbor
local all_conn_info = {}
for _, conn in ipairs(relevant_ports) do
local neighbors = Heat.get_connected_neighbors(ent, conn, false)
for _, neighbor_info in ipairs(neighbors) do
local n_ent = neighbor_info.entity
if n_ent and n_ent.valid then
if not (ent.type == "heat-pipe" and n_ent.type == "heat-pipe") then
-- Use direction from connection_point, fallback to position math
local dir = neighbor_info.connection_point and neighbor_info.connection_point.direction
if dir == nil then
dir = FaUtils.get_direction_biased(
neighbor_info.connection_point and neighbor_info.connection_point.position or n_ent.position,
conn.position
)
else
dir = FaUtils.rotate_180(dir) -- to report it's pointing back at us
end
table.insert(all_conn_info, { n_ent.prototype.localised_name, FaUtils.direction_lookup(dir or 16) })
end
end
end
end
local n = #all_conn_info
if n > 0 then
local key = "fa.ent-info-heat-conn-" .. tostring(math.min(n, 4))
local args = { key }
for i = 1, math.min(n, 4) do
table.insert(args, all_conn_info[i][1]) -- name
table.insert(args, all_conn_info[i][2]) -- direction (raw int)
end
ctx.message:fragment(args)
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_underground_belt_connection(ctx)
local ent = ctx.ent
if ent.type == "underground-belt" then
if ent.neighbours ~= nil then
ctx.message:fragment({
"fa.ent-info-underground-belt-connection",
FaUtils.direction(ent.position, ent.neighbours.position),
math.floor(FaUtils.distance(ent.position, ent.neighbours.position)) - 1,
})
else
ctx.message:fragment({ "fa.ent-info-underground-belt-not-connected" })
end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_splitter_states(ctx)
local ent = ctx.ent
if ent.type == "splitter" then ctx.message:fragment(TransportBelts.splitter_priority_info(ent)) end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_radar(ctx)
local ent = ctx.ent
if ent.type == "radar" then ctx.message:fragment(mod.radar_charting_info(ent)) end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_spidertron(ctx)
local ent = ctx.ent
if ent.type == "spider-leg" then
local spiders =
ent.surface.find_entities_filtered({ position = ent.position, radius = 5, type = "spider-vehicle" })
local spider = ent.surface.get_closest(ent.position, spiders)
if not spider then return end
ent = spider
end
if ent.type == "spider-vehicle" then
local label = ent.entity_label
if label ~= nil then ctx.message:fragment(label) end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_mining_drill_output_chute(ctx)
local point = ResourceMining.get_solid_output_coords(ctx.ent)
if not point then return false end
if util.distance(point.position, ctx.cursor_pos) < 0.6 then
ctx.message:fragment({ "fa.ent-info-mining-drill-output" })
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_cargo_wagon(ctx)
if ctx.ent.name == "cargo-wagon" then
local inv = ctx.ent.get_inventory(defines.inventory.cargo_wagon)
assert(inv)
local presenting = present_list(inv.get_contents())
if presenting then ctx.message:fragment(presenting) end
end
end
---@param ctx fa.Info.EntInfoContext
local function ent_info_fluid_connections(ctx)
local points = Fluids.get_connection_points(ctx.ent)
---@param p fa.Fluids.ConnectionPoint
TH.retain_unordered(points, function(p)
-- If this entity is a pipe and the connection goes to nothing, then do
-- not announce this connection because pipe shapes are handled elsewhere.
if p.raw.target == nil and ctx.ent.type == "pipe" then return false end
if p.raw.target and p.raw.target.owner.type == "pipe" and ctx.ent.type == "pipe" then return false end
return FaUtils.distance(p.position, ctx.cursor_pos) < 0.5
end)
if not next(points) then return end
-- To get convenient announcements, we will roll up into fluids and their
-- directions rather than handling these one by one. If any connection
-- connects to an entity, we instead will handle it separately, so that
-- adjacent entity connections are announced.
-- It is an engine invariant that only one fluidbox may be at any point, in
-- the sense that only one fluid may be handled. Storage tanks are a weird
-- exception which break the documented stricter rule of no fluidboxes
-- sharing a point, as the corners are bidirectional in 2 directions.
local out_dirs = {}
local in_dirs = {}
for _, c in pairs(points) do
if c.output_direction then out_dirs[c.output_direction] = c end
if c.input_direction then in_dirs[c.input_direction] = c end
end
local bidirectionals = {}
for dir, c in pairs(in_dirs) do
local rot = FaUtils.rotate_180(dir)
if out_dirs[rot] then
bidirectionals[rot] = c
in_dirs[dir] = nil
out_dirs[rot] = nil
end
end
local none = {}
local closed = {}
---@param set table<defines.direction, fa.Fluids.ConnectionPoint>
local function present(key, set, rotate)
local buckets = {}
for dir, c in pairs(set) do
local f = c.fluid or none
if not c.open then f = closed end
local realdir = rotate and FaUtils.rotate_180(dir) or dir
local dist = c.distance_in_tiles
buckets[f] = buckets[f] or {}
buckets[f][dist] = buckets[f][dist] or {}
table.insert(buckets[f][dist], {
direction = realdir,
type = c.raw.connection_type,
has_other_side = c.raw.target,
})
end
for fluid, distdirs in pairs(buckets) do
for dist, dirs in pairs(distdirs) do
table.sort(dirs, function(a, b)
return a.direction < b.direction
end)
local dirparts = {}
for _, dirinfo in pairs(dirs) do
local dir = dirinfo.direction
local type = dirinfo.type
if type == "underground" and dirinfo.has_other_side then
table.insert(
dirparts,
{ "fa.ent-info-fluid-connections-via-underground-not-connected", FaUtils.direction_lookup(dir) }
)
else
table.insert(dirparts, FaUtils.direction_lookup(dir))
end
end
local dirlist = FaUtils.localise_cat_table(dirparts, ", ")
---@type LocalisedString
local loc_fluid = { "fa.ent-info-fluid-connections-any" }
if fluid == closed then
loc_fluid = { "fa.ent-info-fluid-connections-closed" }
elseif fluid ~= none then
loc_fluid = Localising.get_localised_name_with_fallback(prototypes.fluid[fluid])
end
ctx.message:list_item({ key, loc_fluid, dirlist, dist })
end
end
end
present("fa.ent-info-fluid-connections-in", in_dirs, true)
present("fa.ent-info-fluid-connections-out", out_dirs)
present("fa.ent-info-fluid-connections-bidirectional", bidirectionals)
end
-- "connects to small electric pole x east, y northwest"
---@param ctx fa.Info.EntInfoContext
local function ent_info_pole_neighbors(ctx)
if ctx.ent.type ~= "electric-pole" then return end
local neighbors = {}
Wires.call_on_connectors(ctx.ent, defines.wire_type.copper, function(_, conn)
---@type LuaEntity
local other = conn.target.owner
local n, q = other.name, other.quality.name
neighbors[n] = neighbors[n] or {}
neighbors[n][q] = neighbors[n][q] or {}
table.insert(neighbors[n][q], other)
return true