-
Notifications
You must be signed in to change notification settings - Fork 41
/
Copy pathresmon.lua
948 lines (809 loc) · 33.1 KB
/
resmon.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
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
require "util"
local ore_tracker = require "libs/ore_tracker"
---@class resmon_base
resmon = {
on_click = {},
-- updated `on_tick` to contain `ore_tracker.get_entity_cache()`
entity_cache = {},
click = require("resmon.click"),
locale = require("resmon.locale"),
migrations = require("resmon.migrations"),
sites = require("resmon.sites"),
types = require("resmon.types"),
ui = require("resmon.ui"),
yatable = require("resmon.yatable"),
}
---Check if `haystack` exactly starts with `needle`; case-sensitive
---@param haystack string
---@param needle string
---@return boolean result True if the first `len(needle)` characters in `haystack` are exactly `needle`
function string.starts_with(haystack, needle)
return string.sub(haystack, 1, string.len(needle)) == needle
end
---Check if `haystack` exactly ends with `needle`; case-sensitive
---@param haystack string
---@param needle string
---@return boolean result True if the last `len(needle)` characters in `haystack` are exactly `needle`
function string.ends_with(haystack, needle)
return string.sub(haystack, -string.len(needle)) == needle
end
---Initialize/upgrade the storage data (for players and forces)
function resmon.init_globals()
resmon.init_storage()
resmon.migrations.perform_migrations()
for _, player in pairs(game.players) do
resmon.init_player(player)
end
end
---Initialize/upgrade the given player
---@param event EventData.on_player_created
function resmon.on_player_created(event)
local player = game.players[event.player_index]
resmon.init_player(player)
end
---YARM v0.11.2: Keeping iter_fn in the site means trying to keep a function in `storage`, which
---blocks saving in Factorio 2.0 and would have possibly also led to some mysterious desyncs in
---previous Factorio versions.
---YARM v1.0: iter_fn is just `next(t, k)`
---@param force_data force_data
local function migrate_remove_iter_fn(force_data)
for _, site in pairs(force_data.ore_sites) do
if site.iter_fn then
site.iter_fn = nil ---@diagnostic disable-line: inject-field
end
end
if resmon.site_iterators then
resmon.site_iterators = nil ---@diagnostic disable-line: inject-field
end
end
---Initialize the player-level persistent data, e.g. overlays.
---Will also trigger force-level initialization
---@param player LuaPlayer
function resmon.init_player(player)
resmon.init_force(player.force --[[@as LuaForce]])
if not storage.player_data then
storage.player_data = {} ---@type player_data[]
end
local player_data = storage.player_data[player.index]
if not player_data then
player_data = resmon.types.new_player_data()
end
storage.player_data[player.index] = player_data
resmon.ui.migrate_player_data(player)
end
---Initialize the force-level stored data, e.g. ore sites
---@param force LuaForce
function resmon.init_force(force)
if not storage.force_data then
storage.force_data = {} ---@type force_data[]
end
local force_data = storage.force_data[force.name]
if not force_data then
force_data = resmon.types.new_force_data()
end
storage.force_data[force.name] = force_data
migrate_remove_iter_fn(force_data)
resmon.sanity_check_sites(force, force_data)
end
---Check if the given table contains the given value as a value
local function table_contains(haystack, needle)
for _, candidate in pairs(haystack) do
if candidate == needle then
return true
end
end
return false
end
---Clean up sites whose resource entities no longer exist, e.g. because of mod removal.
---If any sites were cleaned up, the players on the force owning the site are warned about
---their removal.
---@param force LuaForce
---@param force_data force_data
function resmon.sanity_check_sites(force, force_data)
local discarded_sites = {}
local missing_ores = {}
for name, site in pairs(force_data.ore_sites) do
local entity_prototype = prototypes.entity[site.ore_type]
if not entity_prototype or not entity_prototype.valid then
discarded_sites[#discarded_sites + 1] = name
if not table_contains(missing_ores, site.ore_type) then
missing_ores[#missing_ores + 1] = site.ore_type
end
if site.chart_tag and site.chart_tag.valid then
site.chart_tag.destroy()
end
force_data.ore_sites[name] = nil
end
end
if #discarded_sites == 0 then
return
end
---@type LocalisedString
local message_locale = {
"YARM-warnings.discard-missing-ore-type",
table.concat(discarded_sites, ', '),
table.concat(missing_ores, ', '),
#discarded_sites,
#missing_ores,
}
force.print(message_locale)
log({ "", force.name, " received warning: ", message_locale})
end
resmon.entity_position_to_string = ore_tracker.internal.entity_position_to_string
---Turn a position into a string usable as a table key
---@param pos MapPosition
---@return string
local function position_to_string(pos)
-- scale it up so (hopefully) any floating point component disappears,
-- then force it to be an integer with %d. not using util.positiontostr
-- as it uses %g and keeps the floating point component.
return string.format("%d,%d", pos.x * 100, pos.y * 100)
end
local function find_resource_at(surface, position)
-- The position we get is centered in its tile (e.g., {8.5, 17.5}).
-- Sometimes, the resource does not cover the center, so search the full tile.
local top_left = { x = position.x - 0.5, y = position.y - 0.5 }
local bottom_right = { x = position.x + 0.5, y = position.y + 0.5 }
local stuff = surface.find_entities_filtered { area = { top_left, bottom_right }, type = 'resource' }
if #stuff < 1 then
return nil
end
return stuff[1] -- there should never be another resource at the exact same coordinates
end
local function find_center(area)
local xpos = (area.left + area.right) / 2
local ypos = (area.top + area.bottom) / 2
return { x = xpos, y = ypos }
end
local function find_center_tile(area)
local center = find_center(area)
return { x = math.floor(center.x), y = math.floor(center.y) }
end
---@param event EventData.on_player_selected_area
function resmon.on_player_selected_area(event)
if event.item ~= 'yarm-selector-tool' then
return
end
local player = game.players[event.player_index]
local player_data = storage.player_data[player.index]
local entities = event.entities
if #entities < 1 then
entities = { find_resource_at(event.surface, {
x = 0.5 + math.floor((event.area.left_top.x + event.area.right_bottom.x) / 2),
y = 0.5 + math.floor((event.area.left_top.y + event.area.right_bottom.y) / 2)
}) }
end
if #entities < 1 then
-- if we have an expanding site, submit it. else, just drop the current site
if player_data.current_site and player_data.current_site.is_site_expanding then
resmon.submit_site(player)
else
resmon.clear_current_site(player)
end
return
end
local entities_by_type = {}
for _, entity in pairs(entities) do
if entity.prototype.type == 'resource' then
entities_by_type[entity.name] = entities_by_type[entity.name] or {}
table.insert(entities_by_type[entity.name], entity)
end
end
player_data.todo = player_data.todo or {}
for _, group in pairs(entities_by_type) do table.insert(player_data.todo, group) end
-- note: resmon.update_players() (via on_tick) will continue the operation from here
end
---@param player LuaPlayer
function resmon.clear_current_site(player)
local player_data = storage.player_data[player.index]
player_data.current_site = nil
while #player_data.overlays > 0 do
table.remove(player_data.overlays).destroy()
end
end
---Add a resource to tracking, either creating a new site or expanding the current one
---@param player LuaPlayer
---@param entity LuaEntity
function resmon.add_resource(player, entity)
if not entity.valid then
return
end
local player_data = storage.player_data[player.index]
if player_data.current_site and player_data.current_site.ore_type ~= entity.name then
if player_data.current_site.finalizing then
resmon.submit_site(player)
else
resmon.clear_current_site(player)
end
end
if not player_data.current_site then
player_data.current_site = resmon.types.new_site(player, entity)
end
if player_data.current_site.is_site_expanding then
player_data.current_site.has_expanded = true -- relevant for the console output
if not player_data.current_site.original_amount then
player_data.current_site.original_amount = player_data.current_site.amount
end
end
resmon.add_single_entity(player, entity)
-- note: resmon.scan_current_site() (via on_tick) will continue the operation from here
end
---Add the given entity to the given player's current site
---@param player LuaPlayer
---@param entity LuaEntity
function resmon.add_single_entity(player, entity)
local player_data = storage.player_data[player.index]
local site = player_data.current_site
if not site then
return
end
local tracker_index = ore_tracker.add_entity(entity)
if not tracker_index then
return -- The ore tracker didn't like that entity
end
if site.tracker_indices[tracker_index] then
return -- Don't re-add the same entity (it would mess with the counts)
end
-- Reset the finalizing timer
if site.finalizing then
site.finalizing = false
end
-- Memorize this entity
site.tracker_indices[tracker_index] = true
site.entity_count = site.entity_count + 1
table.insert(site.next_to_scan, entity)
site.amount = site.amount + entity.amount
-- Resize the site bounds if necessary
if entity.position.x < site.extents.left then
site.extents.left = entity.position.x
elseif entity.position.x > site.extents.right then
site.extents.right = entity.position.x
end
if entity.position.y < site.extents.top then
site.extents.top = entity.position.y
elseif entity.position.y > site.extents.bottom then
site.extents.bottom = entity.position.y
end
-- Give visible feedback, too
resmon.put_marker_at(entity.surface, entity.position, player, player_data)
end
---Draw a marker (blue highlight) on top of the given position to signify that
---YARM has seen it (either when scanning for a site creation, or when
---re-displaying an expanding site's known resources). Only the player who
---is creating/expanding the site can see the marker
---@param surface LuaSurface
---@param pos MapPosition
---@param player LuaPlayer Who are we rendering this for
---@param player_data player_data
function resmon.put_marker_at(surface, pos, player, player_data)
if math.floor(pos.x) % settings.global["YARM-overlay-step"].value ~= 0 or
math.floor(pos.y) % settings.global["YARM-overlay-step"].value ~= 0 then
return
end
local overlay = rendering.draw_rectangle {
left_top = { math.floor(pos.x), math.floor(pos.y) },
right_bottom = { math.floor(pos.x + 1), math.floor(pos.y + 1) },
filled = true,
color = { 0, 0, 0.5, 0.4 },
surface = surface,
players = { player },
draw_on_ground = true,
}
table.insert(player_data.overlays, overlay)
end
---Adjust the given coordinates by 1 tile in the given direction
---@param position MapPosition
---@param direction defines.direction
---@return MapPosition position ±1 tile in x, y, or both
local function shift_position(position, direction)
if direction == defines.direction.north then
return { x = position.x, y = position.y - 1 }
elseif direction == defines.direction.northeast then
return { x = position.x + 1, y = position.y - 1 }
elseif direction == defines.direction.east then
return { x = position.x + 1, y = position.y }
elseif direction == defines.direction.southeast then
return { x = position.x + 1, y = position.y + 1 }
elseif direction == defines.direction.south then
return { x = position.x, y = position.y + 1 }
elseif direction == defines.direction.southwest then
return { x = position.x - 1, y = position.y + 1 }
elseif direction == defines.direction.west then
return { x = position.x - 1, y = position.y }
elseif direction == defines.direction.northwest then
return { x = position.x - 1, y = position.y - 1 }
else
return position
end
end
---Continue expanding the current site by scanning near known ores to find new one
---@param player LuaPlayer
function resmon.scan_current_site(player)
local site = storage.player_data[player.index].current_site
if not site then
return
end
local to_scan = math.min(30, #site.next_to_scan)
local max_dist = settings.global["YARM-grow-limit"].value
for i = 1, to_scan do
local entity = table.remove(site.next_to_scan, 1)
if entity and entity.valid then
local entity_position = entity.position
local surface = entity.surface
site.first_center = site.first_center or find_center(site.extents)
-- Look in every direction around this entity...
for _, dir in pairs(defines.direction) do
-- ...and if there's a resource, add it
local search_pos = shift_position(entity_position, dir)
if max_dist < 0 or util.distance(search_pos, site.first_center) < max_dist then
local found = find_resource_at(surface, search_pos)
if found and found.name == site.ore_type then
resmon.add_single_entity(player, found)
end
end
end
end
end
end
---Mark the player's current site as having finished scanning/expanding. This starts the timer
---that will eventually submit the site
---@param player LuaPlayer
function resmon.finalize_site(player)
local player_data = storage.player_data[player.index]
---@type yarm_site
local site = player_data.current_site
site.finalizing = true
site.finalizing_since = game.tick
site.initial_amount = site.amount
site.ore_per_minute = 0
site.remaining_permille = 1000
site.center = find_center_tile(site.extents)
resmon.count_deposits(site, site.added_at % settings.global["YARM-ticks-between-checks"].value)
end
---Submit the player's current site, either adding it to their force's sites or completing
---the site expansion
---@param player LuaPlayer
function resmon.submit_site(player)
local player_data = storage.player_data[player.index]
local force_data = storage.force_data[player.force.name]
local site = player_data.current_site
if not site then
return
end
if site.index == 0 then
site.index = #force_data.ore_sites + 1
end
force_data.ore_sites[site.index] = site
resmon.clear_current_site(player)
if (site.is_site_expanding) then
if (site.has_expanded) then
-- reset statistics, the site didn't actually just grow a bunch of ore in existing tiles
site.last_ore_check = nil
site.last_modified_amount = nil
local amount_added = site.amount - site.original_amount
local sign = amount_added < 0 and '' or '+' -- format_number will handle the negative sign for us (if needed)
player.print { "YARM-site-expanded", site.index, resmon.locale.format_number(site.amount), site.ore_name,
sign .. resmon.locale.format_number(amount_added) }
end
--[[ NB: deliberately not outputting anything in the case where the player cancelled (or
timed out) a site expansion without expanding anything (to avoid console spam) ]]
if site.chart_tag and site.chart_tag.valid then
site.chart_tag.destroy()
end
else
player.print { "YARM-site-submitted", site.index, resmon.locale.format_number(site.amount), site.ore_name }
end
resmon.ui.update_chart_tag(site)
-- clear site expanding state so we can re-expand the same site again (and get sensible numbers!)
if (site.is_site_expanding) then
site.is_site_expanding = nil
site.has_expanded = nil
site.original_amount = nil
end
resmon.ui.update_force_members(player.force)
end
---Sets up or continues counting the resource amounts within the given site. A count will only start
---if the `site.added_at` matches the current update_cycle (to spread site counting across the range
---of `settings.global["YARM-ticks-between-checks"]`). Each tick of counting will only take up to
---1000 resource entities at a time (counting a large site may take multiple ticks).
---@param site yarm_site
---@param update_cycle integer The current tick modulo ticks-between-checks
function resmon.count_deposits(site, update_cycle)
-- the site is already being iterated so just continue iterating
if site.iter_key then
resmon.tick_deposit_count(site)
return
end
-- the site is not being iterated; is it time to do so?
local site_update_cycle = site.added_at % settings.global["YARM-ticks-between-checks"].value
if site_update_cycle ~= update_cycle then
return
end
-- yes, it's time to iterate it; set up the state and get it going!
site.iter_key = nil
site.update_amount = 0
resmon.tick_deposit_count(site)
end
---Count up to 1000 resources in the given site, adding them to the update_amount. If this tick
---finished all the resources in the site, continues with `resmon.finish_deposit_count`
---@param site yarm_site
function resmon.tick_deposit_count(site)
local index = site.iter_key
for _ = 1, 1000 do
index = next(site.tracker_indices, index)
if index == nil then
resmon.finish_deposit_count(site)
return
end
local tracking_data = resmon.entity_cache[index]
if tracking_data and tracking_data.valid then
site.update_amount = site.update_amount + tracking_data.resource_amount
else
site.tracker_indices[index] = nil -- It's permitted to delete from a table being iterated
site.entity_count = site.entity_count - 1
end
end
site.iter_key = index
end
---As a default case, takes a diff between two values and returns a smoothed
---easing step. However, to force convergence it does *not* smooth diffs below 1
---and clamps smoothed diffs below 10 to be at least 1.
function resmon.smooth_clamp_diff(diff)
if math.abs(diff) < 1 then
return diff
elseif math.abs(diff) < 10 then
return math.abs(diff) / diff
end
return 0.1 * diff
end
local function isnan(n)
return type(n) == "number" and n ~= n
end
---Update the site ore counts and depletion rate/time
---@param site yarm_site
function resmon.finish_deposit_count(site)
site.iter_key = nil
if site.last_ore_check then
if not site.last_modified_amount then
-- make sure those two values have a default
site.last_modified_amount = site.amount --
site.last_modified_tick = site.last_ore_check --
end
local delta_ore_since_last_update = site.last_modified_amount - site.amount
if delta_ore_since_last_update ~= 0 then
-- only store the amount and tick from last update if it actually changed
site.last_modified_tick = site.last_ore_check
site.last_modified_amount = site.amount
end
local delta_ore_since_last_change = (site.update_amount - site.last_modified_amount)
local delta_ticks = game.tick - site.last_modified_tick
if delta_ticks > 0 then
-- we don't want a nan to gum up the works
local new_ore_per_minute = (delta_ore_since_last_change * 3600 / delta_ticks)
local diff_step = resmon.smooth_clamp_diff(new_ore_per_minute - site.scanned_ore_per_minute)
site.scanned_ore_per_minute = site.scanned_ore_per_minute + diff_step
end
if isnan(site.scanned_ore_per_minute) then
-- but if a nan happened to get in, fix it
site.scanned_ore_per_minute = 0
end
end
local entity_prototype = prototypes.entity[site.ore_type]
local is_endless = entity_prototype.infinite_resource
local minimum = is_endless and (site.entity_count * entity_prototype.minimum_resource_amount) or 0
local amount_left = site.amount - minimum
site.scanned_etd_minutes =
(site.scanned_ore_per_minute ~= 0 and amount_left / (-site.scanned_ore_per_minute))
or (amount_left == 0 and 0)
or -1
site.amount = site.update_amount
amount_left = site.amount - minimum
site.amount_left = amount_left
if settings.global["YARM-adjust-over-percentage-sites"].value then
site.initial_amount = math.max(site.initial_amount, site.amount)
end
site.last_ore_check = game.tick
site.remaining_permille = resmon.calc_remaining_permille(site)
local age_minutes = (game.tick - site.added_at) / 3600
local depleted = site.initial_amount - site.amount
site.lifetime_ore_per_minute = -depleted / age_minutes
site.lifetime_etd_minutes =
(site.lifetime_ore_per_minute ~= 0 and amount_left / (-site.lifetime_ore_per_minute))
or (amount_left == 0 and 0)
or -1
local old_etd_minutes = site.etd_minutes
local old_ore_per_minute = site.ore_per_minute
if site.scanned_etd_minutes == -1 or site.lifetime_etd_minutes <= site.scanned_etd_minutes then
site.ore_per_minute = site.lifetime_ore_per_minute
site.etd_minutes = site.lifetime_etd_minutes
site.etd_is_lifetime = true
else
site.ore_per_minute = site.scanned_ore_per_minute
site.etd_minutes = site.scanned_etd_minutes
site.etd_is_lifetime = false
end
site.etd_minutes_delta = site.etd_minutes - old_etd_minutes
site.ore_per_minute_delta = site.ore_per_minute - old_ore_per_minute
-- these are just to prevent errant NaNs
site.etd_minutes_delta = (site.etd_minutes_delta ~= site.etd_minutes_delta) and 0 or site.etd_minutes_delta
site.ore_per_minute_delta =
(site.ore_per_minute_delta ~= site.ore_per_minute_delta) and 0 or site.ore_per_minute_delta
resmon.ui.update_chart_tag(site)
script.raise_event(on_site_updated, {
force_name = site.force.name,
site_name = site.index,
name_tag = site.name_tag,
amount = site.amount,
ore_per_minute = site.ore_per_minute,
remaining_permille = site.remaining_permille,
ore_type = site.ore_type,
etd_minutes = site.etd_minutes,
})
end
---Determine a site's remaining permille based on its current and initial amount. Infinite
---resources count down to their minimum_resource_amount rather than 0
---@param site yarm_site
---@return number # 0-1000 describing how full the site is compared to its initial amount
function resmon.calc_remaining_permille(site)
local entity_prototype = prototypes.entity[site.ore_type]
local minimum = entity_prototype.infinite_resource
and (site.entity_count * entity_prototype.minimum_resource_amount) or 0
local amount_left = site.amount - minimum
local initial_amount_available = site.initial_amount - minimum
return initial_amount_available <= 0 and 0 or math.floor(amount_left * 1000 / initial_amount_available)
end
function resmon.surface_names()
local names = {}
for _, surface in pairs(game.surfaces) do
table.insert(names, surface.name)
end
return names
end
---@param event EventData.on_gui_confirmed
function resmon.on_gui_confirmed(event)
if not event.element or not event.element.valid then
return
end
if event.element.name ~= "new_name" or event.element.parent.name ~= "YARM_site_rename" then
return
end
resmon.click.handlers.YARM_rename_confirm(event)
end
---@param event EventData.on_gui_closed
function resmon.on_gui_closed(event)
if event.gui_type ~= defines.gui_type.custom then
return
end
if not event.element or not event.element.valid then
return
end
if event.element.name ~= "YARM_site_rename" then
return
end
resmon.click.handlers.YARM_rename_cancel(event)
end
---@param event EventData.CustomInputEvent
function resmon.on_get_selection_tool(event)
local player = game.players[event.player_index]
resmon.give_selection_tool(player)
end
---Give the player the YARM selector tool
---@param player LuaPlayer
function resmon.give_selection_tool(player)
if player.cursor_stack.valid_for_read then
-- already have something?
if player.cursor_stack.name == "yarm-selector-tool" then
return
end
player.clear_cursor() -- and it's not a selector tool, so Q it away
end
player.cursor_stack.set_stack { name = "yarm-selector-tool" }
end
---Set up the current site to have its entities highlighted (for a site expansion)
---@param player LuaPlayer
function resmon.start_recreate_overlay_existing_site(player)
local site = storage.player_data[player.index].current_site
if not site then
return
end
site.is_overlay_being_created = true
-- forcible cleanup in case we got interrupted during a previous background overlay attempt
site.entities_to_be_overlaid = {}
site.entities_to_be_overlaid_count = 0
site.next_to_overlay = {}
site.next_to_overlay_count = 0
for index in pairs(site.tracker_indices) do
local tracking_data = resmon.entity_cache[index]
if tracking_data then
local ent = tracking_data.entity
if ent and ent.valid then
local key = position_to_string(ent.position)
site.entities_to_be_overlaid[key] = ent.position
site.entities_to_be_overlaid_count = site.entities_to_be_overlaid_count + 1
end
end
end
end
---Intermediate step in highlighting existing entities for the current site
---@param player LuaPlayer
function resmon.process_overlay_for_existing_site(player)
local player_data = storage.player_data[player.index]
local site = player_data.current_site
if not site then
return
end
if site.next_to_overlay_count == 0 then
if site.entities_to_be_overlaid_count == 0 then
resmon.end_overlay_creation_for_existing_site(player)
return
else
local ent_key, ent_pos = next(site.entities_to_be_overlaid)
site.next_to_overlay[ent_key] = ent_pos
site.next_to_overlay_count = site.next_to_overlay_count + 1
end
end
local to_scan = math.min(30, site.next_to_overlay_count)
for _ = 1, to_scan do
resmon.overlay_next_entity_in_existing_site(site, player, player_data)
end
end
---Overlay the next entity in the given site and scan around it for more
---@param site yarm_site
---@param player LuaPlayer
---@param player_data player_data
function resmon.overlay_next_entity_in_existing_site(site, player, player_data)
local ent_key, ent_pos = next(site.next_to_overlay)
local entity = site.surface.find_entity(site.ore_type, ent_pos)
if not entity or not entity.valid then
return
end
-- put marker down
resmon.put_marker_at(site.surface, entity.position, player, player_data)
-- remove it from our to-do lists
site.entities_to_be_overlaid[ent_key] = nil
site.entities_to_be_overlaid_count = site.entities_to_be_overlaid_count - 1
site.next_to_overlay[ent_key] = nil
site.next_to_overlay_count = site.next_to_overlay_count - 1
-- Look in every direction around this entity...
for _, dir in pairs(defines.direction) do
-- ...and if there's a resource that's not already overlaid, add it
local found = find_resource_at(site.surface, shift_position(entity.position, dir))
if found and found.name == site.ore_type then
local offsetkey = position_to_string(found.position)
if site.entities_to_be_overlaid[offsetkey] ~= nil and site.next_to_overlay[offsetkey] == nil then
site.next_to_overlay[offsetkey] = found.position
site.next_to_overlay_count = site.next_to_overlay_count + 1
end
end
end
end
---Final step in creating overlay for existing site, set it back to the finalizing stage
---@param player LuaPlayer
function resmon.end_overlay_creation_for_existing_site(player)
local site = storage.player_data[player.index].current_site
if not site then
return
end
site.is_overlay_being_created = false
site.finalizing = true
site.finalizing_since = game.tick
end
---@param event EventData.on_tick
function resmon.update_players(event)
-- At tick 0 on an MP server initial join, on_init may not have run
if not storage.player_data then
return
end
for _, player in pairs(game.players) do
local player_data = storage.player_data[player.index]
if not player_data then
resmon.init_player(player)
elseif not player.connected and player_data.current_site then
resmon.clear_current_site(player)
end
if player_data.current_site then
local site = player_data.current_site --[[@as yarm_site]]
if #site.next_to_scan > 0 then
resmon.scan_current_site(player)
elseif not site.finalizing then
resmon.finalize_site(player)
elseif site.finalizing_since + 120 == event.tick then
resmon.submit_site(player)
end
if site.is_overlay_being_created then
resmon.process_overlay_for_existing_site(player)
end
else
local todo = player_data.todo or {}
if #todo > 0 then
for _, entity in pairs(table.remove(todo)) do
resmon.add_resource(player, entity)
end
end
end
local update_ticks = player.mod_settings["YARM-hud-update-ticks"].value or 300
if event.tick % update_ticks == 15 + player.index then
resmon.ui.update_player(player)
end
end
end
---@param event EventData.on_tick
function resmon.update_forces(event)
-- At tick 0 on an MP server initial join, on_init may not have run
if not storage.force_data then
return
end
local update_cycle = event.tick % settings.global["YARM-ticks-between-checks"].value
for _, force in pairs(game.forces) do
local force_data = storage.force_data[force.name]
if not force_data then
resmon.init_force(force)
elseif force_data and force_data.ore_sites then
for _, site in pairs(force_data.ore_sites) do
if site.deleting_since and site.deleting_since + 120 < game.tick then
site.deleting_since = nil
end
resmon.count_deposits(site, update_cycle)
end
end
end
end
local function profiler_output(message, stopwatch)
local output = { "", message, " - ", stopwatch }
log(output)
for _, player in pairs(game.players) do
player.print(output)
end
end
---@param event EventData.on_tick
local function on_tick_internal(event)
ore_tracker.on_tick(event)
resmon.entity_cache = ore_tracker.get_entity_cache()
resmon.update_players(event)
resmon.update_forces(event)
end
---@param event EventData.on_tick
local function on_tick_internal_with_profiling(event)
local big_stopwatch = game.create_profiler()
local stopwatch = game.create_profiler()
ore_tracker.on_tick(event)
stopwatch.stop()
profiler_output("ore_tracker", stopwatch)
resmon.entity_cache = ore_tracker.get_entity_cache()
stopwatch.reset()
resmon.update_players(event)
stopwatch.stop()
profiler_output("update_players", stopwatch)
stopwatch.reset()
resmon.update_forces(event)
stopwatch.stop()
profiler_output("update_forces", stopwatch)
big_stopwatch.stop()
profiler_output("total on_tick", big_stopwatch)
end
---@param event EventData.on_tick
function resmon.on_tick(event)
local wants_profiling = settings.global["YARM-debug-profiling"].value or false
if wants_profiling then
on_tick_internal_with_profiling(event)
else
on_tick_internal(event)
end
end
function resmon.on_load()
ore_tracker.on_load()
end
function resmon.init_storage()
-- NB: storage.player_data and storage.force_data are carefully chosen:
-- - if upgrading from YARM < v1.0, it already exists so we must not initialize storage.versions
-- - if it's a completely new game, it doesn't exist and we can initialize storage.versions
-- - if it's not a completely new game and YARM >= 1.0, it exists and so does storage.versions
if not storage.player_data or not storage.force_data then
storage = {
versions = resmon.migrations.default_versions(),
force_data = {}, ---@type force_data[]
player_data = {}, ---@type player_data[]
}
end
ore_tracker.init_globals()
end