Skip to content

Commit 0d4ccda

Browse files
committed
Implements new ActiveJobManager class
1 parent dc28944 commit 0d4ccda

File tree

2 files changed

+433
-0
lines changed

2 files changed

+433
-0
lines changed
Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
#include <active-job-manager.h>
2+
#include <inlines.h>
3+
#include <ranges>
4+
5+
#include <tile-cache.h>
6+
#include <Debug.h>
7+
#include <PluginManager.h>
8+
9+
#include <modules/Units.h>
10+
#include <df/block_square_event_designation_priorityst.h>
11+
#include <df/report.h>
12+
13+
namespace CSP {
14+
extern std::unordered_set<df::coord> dignow_queue;
15+
}
16+
17+
df::coord simulate_fall(const df::coord &pos) {
18+
if unlikely(!Maps::isValidTilePos(pos)) {
19+
ERR(plugin).print("Error: simulate_fall(" COORD ") - invalid coordinate\n", COORDARGS(pos));
20+
return {};
21+
}
22+
df::coord resting_pos(pos);
23+
24+
while (Maps::ensureTileBlock(resting_pos)) {
25+
df::tiletype tt = *Maps::getTileType(resting_pos);
26+
if (isWalkable(tt))
27+
break;
28+
--resting_pos.z;
29+
}
30+
31+
return resting_pos;
32+
}
33+
34+
df::coord simulate_area_fall(const df::coord &pos) {
35+
df::coord neighbours[8]{};
36+
get_neighbours(pos, neighbours);
37+
df::coord lowest = simulate_fall(pos);
38+
for (auto p : neighbours) {
39+
if unlikely(!Maps::isValidTilePos(p)) continue;
40+
auto nlow = simulate_fall(p);
41+
if (nlow.z < lowest.z) {
42+
lowest = nlow;
43+
}
44+
}
45+
return lowest;
46+
}
47+
48+
bool ActiveJobManager::has_cavein_conditions(const df::coord &map_pos) const {
49+
auto p = map_pos;
50+
auto ttype = *Maps::getTileType(p);
51+
if (!DFHack::isOpenTerrain(ttype)) {
52+
// check shared neighbour for cave-in conditions
53+
df::coord neighbours[4];
54+
get_connected_neighbours(map_pos, neighbours);
55+
int connectedness = 4;
56+
for (auto n: neighbours) {
57+
if (!Maps::isValidTilePos(n) || active_dig_sites.count(n) || DFHack::isOpenTerrain(*Maps::getTileType(n))) {
58+
connectedness--;
59+
}
60+
}
61+
if (!connectedness) {
62+
// do what?
63+
p.z--;
64+
if (!Maps::isValidTilePos(p)) return false;
65+
ttype = *Maps::getTileType(p);
66+
if (DFHack::isOpenTerrain(ttype) || DFHack::isFloorTerrain(ttype)) {
67+
return true;
68+
}
69+
}
70+
}
71+
return false;
72+
}
73+
74+
bool ActiveJobManager::possible_cavein(const df::coord &map_pos) const {
75+
for (auto dig_pos : active_dig_sites) {
76+
if (dig_pos == map_pos) continue;
77+
if (calc_distance(map_pos, dig_pos) <= 2) {
78+
// find neighbours
79+
df::coord n1[8];
80+
df::coord n2[8];
81+
get_neighbours(map_pos, n1);
82+
get_neighbours(dig_pos, n2);
83+
// find shared neighbours
84+
for (int i = 0; i < 7; ++i) {
85+
for (int j = i + 1; j < 8; ++j) {
86+
if (n1[i] == n2[j]) {
87+
if (has_cavein_conditions(n1[i])) {
88+
WARN(jobs).print("Channel-Safely::jobs: Cave-in conditions detected at (" COORD ")\n", COORDARGS(n1[i]));
89+
return true;
90+
}
91+
}
92+
}
93+
}
94+
}
95+
}
96+
return false;
97+
}
98+
99+
void ActiveJobManager::cleanup() {
100+
if (active_jobs.empty()) {
101+
return;
102+
}
103+
// make two sets. 1 for valid active job ids. 1 for valid active job dig sites.
104+
std::unordered_set<int32_t> valid_job_ids;
105+
std::unordered_set<df::coord> valid_dig_sites;
106+
// iterate over valid jobs
107+
for (df::job_list_link* node = &df::global::world->jobs.list; node != nullptr; node = node->next) {
108+
if (df::job* job = node->item; active_jobs.contains(job->id)) {
109+
valid_job_ids.emplace(job->id);
110+
valid_dig_sites.emplace(job->pos);
111+
}
112+
}
113+
114+
// erase stale data
115+
std::erase_if(active_jobs,[&](const std::pair<int32_t, ActiveJob> &kv) {
116+
return !valid_job_ids.contains(kv.first);
117+
});
118+
std::erase_if(active_workers, [&](const std::pair<int32_t, ActiveWorker> &kv) {
119+
return !valid_job_ids.contains(kv.first);
120+
});
121+
std::erase_if(active_dig_sites,[&](const df::coord &pos) {
122+
return !valid_dig_sites.contains(pos);
123+
});
124+
std::erase_if(cancel_queue, [&](const df::coord &pos) {
125+
return !active_dig_sites.contains(pos);
126+
});
127+
// clean up any "endangered" workers that have been tracked for longer than the configured duration
128+
std::erase_if(endangered_units, [tick = df::global::world->frame_counter](const std::pair<int32_t,int32_t> &kv) {
129+
return tick - kv.second > config.res_watch_duration;
130+
});
131+
// erase cancellation data
132+
std::erase_if(active_jobs,[&](const std::pair<int32_t, ActiveJob> &kv) {
133+
return cancel_queue.contains(kv.second.pos);
134+
});
135+
std::erase_if(active_workers, [&](const std::pair<int32_t, ActiveWorker> &kv) {
136+
return !active_jobs.contains(kv.first);
137+
});
138+
std::erase_if(active_dig_sites, [&](const df::coord &pos) {
139+
return cancel_queue.contains(pos);
140+
});
141+
}
142+
143+
void ActiveJobManager::on_update(color_ostream &out) {
144+
int32_t tick = df::global::world->frame_counter;
145+
// clean up stale df::job*
146+
if ((config.monitoring || config.resurrect) && tick - last_tick >= 1) {
147+
last_tick = tick;
148+
cleanup();
149+
}
150+
// cancel jobs in the cancel queue
151+
for (auto pos : cancel_queue) {
152+
cancel_job(pos);
153+
if (!ChannelManager::Get().manage_one(pos, true, true)) {
154+
DEBUG(jobs).print(" <- JobStartedEvent(): failed to cancel a job and marker the designation.");
155+
}
156+
}
157+
cancel_queue.clear();
158+
159+
// monitoring activity
160+
if (config.monitoring && tick - last_monitor_tick >= config.monitor_freq) {
161+
last_monitor_tick = tick;
162+
TRACE(monitor).print("OnUpdate() monitoring now\n");
163+
164+
// iterate active jobs
165+
for (auto& [id,ajob]: active_jobs) {
166+
if unlikely(!ajob.worker) continue;
167+
if unlikely(!Units::isAlive(ajob.worker)) continue;
168+
if unlikely(!Maps::isValidTilePos(ajob.pos)) continue;
169+
170+
// check for fall safety
171+
if (ajob.worker->pos == ajob.pos && !is_safe_fall(ajob.pos)) {
172+
// unsafe
173+
WARN(monitor).print(" -> unsafe job\n");
174+
Job::removeWorker(ajob.job);
175+
176+
// decide to insta-dig, marker mode, or break a few eggs to get it done before unbreaking them
177+
if (config.insta_dig) {
178+
// delete the job
179+
Job::removeJob(ajob.job);
180+
// queue digging the job instantly
181+
CSP::dignow_queue.emplace(ajob.pos);
182+
DEBUG(monitor).print(" -> insta-dig\n");
183+
} else if (!config.resurrect) {
184+
// set marker mode
185+
Maps::getTileOccupancy(ajob.pos)->bits.dig_marked = true;
186+
187+
using df_bsedp = df::block_square_event_designation_priorityst;
188+
// prevent algorithm from re-enabling designation
189+
for (auto &blk_evt: Maps::getBlock(ajob.pos)->block_events) {
190+
if (auto bsedp = virtual_cast<df_bsedp>(blk_evt)) {
191+
df::coord local(ajob.pos);
192+
local.x = local.x % 16;
193+
local.y = local.y % 16;
194+
bsedp->priority[Coord(local)] = config.ignore_threshold * 1000 + 1;
195+
break;
196+
}
197+
}
198+
DEBUG(monitor).print(" -> set marker mode\n");
199+
}
200+
}
201+
}
202+
TRACE(monitor).print("OnUpdate() monitoring done\n");
203+
}
204+
205+
// Resurrect Dead Workers
206+
if (config.resurrect && tick - last_resurrect_tick >= 1) {
207+
last_resurrect_tick = tick;
208+
for (auto [id, aworker] : active_workers) {
209+
if (Units::isAlive(aworker.worker)) {
210+
continue;
211+
}
212+
resurrect(out, aworker.id);
213+
df::coord lowest = simulate_fall(aworker.last_safe_pos);
214+
Units::teleport(aworker.worker, lowest);
215+
}
216+
// resurrect any dead endangered units
217+
for (auto unit : df::global::world->units.all) {
218+
if (!endangered_units.contains(unit->id) || !safe_locations.contains(unit->id)) {
219+
continue;
220+
}
221+
if (Units::isAlive(unit)) {
222+
continue;
223+
}
224+
resurrect(out, unit->id);
225+
df::coord lowest = simulate_fall(safe_locations[unit->id]);
226+
Units::teleport(unit, lowest);
227+
}
228+
}
229+
}
230+
231+
void ActiveJobManager::on_job_start(df::job* job) {
232+
if (!ChannelManager::Get().exists(job->pos)) {
233+
ChannelManager::Get().build_groups(false);
234+
}
235+
df::unit* worker = Job::getWorker(job);
236+
// there is a valid worker (living citizen) on the job? right..
237+
if unlikely(!worker || !Units::isAlive(worker) || !Units::isCitizen(worker)) {
238+
DEBUG(jobs).print("on_job_start: invalid worker, function exits early.");
239+
return;
240+
}
241+
auto pos = job->pos;
242+
ActiveJob ajob {job->id, job, worker, pos};
243+
ActiveWorker aworker {worker->id, worker, worker->pos};
244+
// we only track ActiveJob's when monitoring or resurrecting
245+
if (config.monitoring || config.resurrect) {
246+
active_jobs.emplace(ajob.id,ajob);
247+
active_workers.emplace(ajob.id, aworker);
248+
safe_locations[aworker.id] = aworker.last_safe_pos;
249+
}
250+
// cavein prevention is the rest of the function
251+
if (!config.riskaverse) {
252+
return;
253+
}
254+
// if a cavein is possible - we'll try to cancel the job
255+
if (possible_cavein(pos)) {
256+
/* todo:
257+
* test if the game crashes or the jobs start polluting the list indefinitely
258+
* prediction is that the jobs will cause the tiles to flash forever
259+
*/
260+
if (remove_worker(job) == 0) DEBUG(jobs).print(" Unable to remove worker from job.");
261+
cancel_queue.emplace(pos);
262+
return;
263+
}
264+
// manage the group the job belongs to
265+
ChannelManager::Get().manage_group(pos, true, false);
266+
// we track active dig sites for cavein detection
267+
active_dig_sites.emplace(pos);
268+
// set tile to restricted
269+
TRACE(jobs).print(" setting job tile to restricted\n");
270+
Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Restricted;
271+
}
272+
273+
void ActiveJobManager::on_job_completed(color_ostream &out, df::job* job) {
274+
if (!active_jobs.contains(job->id)) {
275+
return;
276+
}
277+
auto ajob = active_jobs[job->id];
278+
auto aworker = active_workers[ajob.id];
279+
if (config.resurrect && !Units::isAlive(aworker.worker)) {
280+
resurrect(out, aworker.id);
281+
df::coord lowest = simulate_fall(aworker.last_safe_pos);
282+
Units::teleport(aworker.worker, lowest);
283+
}
284+
285+
// verify completion
286+
auto block = Maps::getTileBlock(ajob.pos);
287+
df::coord local(ajob.pos);
288+
local.x = local.x % 16;
289+
local.y = local.y % 16;
290+
if (!TileCache::Get().hasChanged(ajob.pos, block->tiletype[Coord(local)])) {
291+
return;
292+
}
293+
// the job can be considered done
294+
ChannelManager::Get().mark_done(ajob.pos);
295+
ChannelManager::Get().manage_group(ajob.pos, true, false);
296+
block->designation[Coord(local)].bits.traffic = df::tile_traffic::Normal;
297+
df::coord below(ajob.pos);
298+
below.z--;
299+
DEBUG(jobs).print(" -> (" COORD ") is marked done, managing group below.\n", COORDARGS(ajob.pos));
300+
ChannelManager::Get().manage_group(below);
301+
302+
// erase tracked data
303+
active_jobs.erase(ajob.id);
304+
active_workers.erase(ajob.id);
305+
active_dig_sites.erase(ajob.pos);
306+
TileCache::Get().uncache(ajob.pos);
307+
//CSP::dignow_queue.erase(ajob.pos);
308+
}
309+
310+
void ActiveJobManager::on_report_event(df::report* report) {
311+
int32_t tick = df::global::world->frame_counter;
312+
switch (report->type) {
313+
case announcement_type::CANCEL_JOB:
314+
if (config.insta_dig) {
315+
if (report->text.find("cancels Dig") != std::string::npos ||
316+
report->text.find("path") != std::string::npos) {
317+
318+
CSP::dignow_queue.emplace(report->pos);
319+
}
320+
DEBUG(plugin).print("%d, pos: " COORD ", pos2: " COORD "\n%s\n", report->id, COORDARGS(report->pos),
321+
COORDARGS(report->pos2), report->text.c_str());
322+
}
323+
break;
324+
case announcement_type::CAVE_COLLAPSE:
325+
if (config.resurrect) {
326+
DEBUG(plugin).print("CAVE IN\n%d, pos: " COORD ", pos2: " COORD "\n%s\n", report->id, COORDARGS(report->pos),
327+
COORDARGS(report->pos2), report->text.c_str());
328+
329+
df::coord below = report->pos;
330+
below.z -= 1;
331+
below = simulate_area_fall(below);
332+
df::coord areaMin{report->pos};
333+
df::coord areaMax{areaMin};
334+
areaMin.x -= 15;
335+
areaMin.y -= 15;
336+
areaMax.x += 15;
337+
areaMax.y += 15;
338+
areaMin.z = below.z;
339+
areaMax.z += 1;
340+
std::vector<df::unit*> units;
341+
Units::getUnitsInBox(units, COORDARGS(areaMin), COORDARGS(areaMax));
342+
for (auto unit: units) {
343+
endangered_units[unit->id] = tick;
344+
DEBUG(plugin).print(" [id %d] was near a cave in.\n", unit->id);
345+
if (safe_locations.emplace(unit->id, unit->pos).second) {
346+
DEBUG(plugin).print(" [id %d] doesn't have a safe location saved, we've saved their current position.\n", unit->id);
347+
}
348+
}
349+
for (auto [job_id,aworker] : active_workers) {
350+
if (endangered_units.contains(aworker.id)) {
351+
endangered_units[aworker.id] = tick;
352+
DEBUG(plugin).print(" [id %d] is/was an endangereed worker, we'll extend tracking them too.\n", aworker.id);
353+
}
354+
}
355+
}
356+
break;
357+
default:
358+
break;
359+
}
360+
}
361+
362+
void ActiveJobManager::clear() {
363+
safe_locations.clear();
364+
endangered_units.clear();
365+
active_dig_sites.clear();
366+
active_workers.clear();
367+
active_jobs.clear();
368+
cancel_queue.clear();
369+
}
370+

0 commit comments

Comments
 (0)