diff --git a/library/modules/Job.cpp b/library/modules/Job.cpp index 4e8094547..80c332fc8 100644 --- a/library/modules/Job.cpp +++ b/library/modules/Job.cpp @@ -175,10 +175,12 @@ static void print_job_item_details(Core *c, df::job *job, unsigned idx, df::job_ void DFHack::printJobDetails(Core *c, df::job *job) { + c->con.color(job->flags.bits.suspend ? Console::COLOR_DARKGREY : Console::COLOR_GREY); c->con << "Job " << job->id << ": " << ENUM_KEY_STR(job_type,job->job_type); if (job->flags.whole) c->con << " (" << bitfieldToString(job->flags) << ")"; c->con << endl; + c->con.reset_color(); df::item_type itype = ENUM_ATTR(job_type, item, job->job_type); diff --git a/plugins/workflow.cpp b/plugins/workflow.cpp index a874cd45a..369f9947c 100644 --- a/plugins/workflow.cpp +++ b/plugins/workflow.cpp @@ -20,11 +20,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -73,9 +75,9 @@ DFhackCExport command_result plugin_init (Core *c, std::vector & " List workflow-controlled jobs (if in a workshop, filtered by it).\n" " workflow list\n" " List active constraints, and their job counts.\n" - " workflow limit [cnt-gap]\n" - " workflow limit-count [cnt-gap]\n" - " Set a constraint. The second form counts each stack as 1 item.\n" + " workflow count [cnt-gap]\n" + " workflow amount [cnt-gap]\n" + " Set a constraint. The first form counts each stack as only 1 item.\n" " workflow unlimit \n" " Delete a constraint.\n" "Function:\n" @@ -88,23 +90,23 @@ DFhackCExport command_result plugin_init (Core *c, std::vector & " much below the limit the amount has to drop before jobs are resumed;\n" " this is intended to reduce the frequency of jobs being toggled.\n" "Constraint examples:\n" - " workflow limit AMMO:ITEM_AMMO_BOLTS/METAL 1000 100\n" - " workflow limit AMMO:ITEM_AMMO_BOLTS/WOOD,BONE 200 50\n" + " workflow amount AMMO:ITEM_AMMO_BOLTS/METAL 1000 100\n" + " workflow amount AMMO:ITEM_AMMO_BOLTS/WOOD,BONE 200 50\n" " Keep metal bolts within 900-1000, and wood/bone within 150-200.\n" - " workflow limit-count FOOD 120 30\n" - " workflow limit-count DRINK 120 30\n" + " workflow count FOOD 120 30\n" + " workflow count DRINK 120 30\n" " Keep the number of prepared food & drink stacks between 90 and 120\n" - " workflow limit BIN 30\n" - " workflow limit BARREL 30\n" - " workflow limit BOX/CLOTH,SILK,YARN 30\n" + " workflow count BIN 30\n" + " workflow count BARREL 30\n" + " workflow count BOX/CLOTH,SILK,YARN 30\n" " Make sure there are always 25-30 empty bins/barrels/bags.\n" - " workflow limit BAR//COAL 20\n" - " workflow limit BAR//COPPER 30\n" + " workflow count BAR//COAL 20\n" + " workflow count BAR//COPPER 30\n" " Make sure there are always 15-20 coal and 25-30 copper bars.\n" - " workflow limit-count POWDER_MISC/SAND 20\n" - " workflow limit-count BOULDER/CLAY 20\n" + " workflow count POWDER_MISC/SAND 20\n" + " workflow count BOULDER/CLAY 20\n" " Collect 15-20 sand bags and clay boulders.\n" - " workflow limit POWDER_MISC//MUSHROOM_CUP_DIMPLE:MILL 100 20\n" + " workflow amount POWDER_MISC//MUSHROOM_CUP_DIMPLE:MILL 100 20\n" " Make sure there are always 80-100 units of dimple dye.\n" " In order for this to work, you have to set the material of\n" " the PLANT input on the Mill Plants job to MUSHROOM_CUP_DIMPLE\n" @@ -142,32 +144,46 @@ DFhackCExport command_result plugin_onstatechange(Core* c, state_change_event ev return CR_OK; } -/*******************************/ +/****************************** + * JOB STATE TRACKING STRUCTS * + ******************************/ + +const int DAY_TICKS = 1200; +const int MONTH_DAYS = 28; +const int YEAR_MONTHS = 12; struct ItemConstraint; struct ProtectedJob { int id; int building_id; - int check_idx; + int tick_idx; + + static int cur_tick_idx; - bool live; df::building *holder; df::job *job_copy; int reaction_id; df::job *actual_job; + bool want_resumed; + int resume_time, resume_delay; + std::vector constraints; - ProtectedJob(df::job *job) : id(job->id), live(true) +public: + ProtectedJob(df::job *job) : id(job->id) { - check_idx = 0; + tick_idx = cur_tick_idx; holder = getJobHolder(job); building_id = holder ? holder->id : -1; job_copy = cloneJobStruct(job); actual_job = job; reaction_id = -1; + + want_resumed = false; + resume_time = 0; resume_delay = DAY_TICKS; } ~ProtectedJob() @@ -175,6 +191,14 @@ struct ProtectedJob { deleteJobStruct(job_copy); } + bool isActuallyResumed() { + return actual_job && !actual_job->flags.bits.suspend; + } + bool isResumed() { + return want_resumed || isActuallyResumed(); + } + bool isLive() { return actual_job != NULL; } + void update(df::job *job) { actual_job = job; @@ -185,8 +209,60 @@ struct ProtectedJob { deleteJobStruct(job_copy); job_copy = cloneJobStruct(job); } + + void tick_job(df::job *job, int ticks) + { + tick_idx = cur_tick_idx; + actual_job = job; + + if (isActuallyResumed()) + { + resume_time = 0; + resume_delay = std::max(DAY_TICKS, resume_delay - ticks); + } + else if (want_resumed) + { + if (!resume_time) + want_resumed = false; + else if (world->frame_counter >= resume_time) + actual_job->flags.bits.suspend = false; + } + } + + void recover(df::job *job) + { + actual_job = job; + job->flags.bits.repeat = true; + job->flags.bits.suspend = true; + + resume_delay = std::min(DAY_TICKS*MONTH_DAYS, 5*resume_delay/3); + resume_time = world->frame_counter + resume_delay; + } + + void set_resumed(bool resume) + { + want_resumed = resume; + + if (resume) + { + if (world->frame_counter >= resume_time) + actual_job->flags.bits.suspend = false; + } + else + { + if (isActuallyResumed()) + { + resume_time = 0; + resume_delay = DAY_TICKS; + } + + actual_job->flags.bits.suspend = true; + } + } }; +int ProtectedJob::cur_tick_idx = 0; + typedef std::map, bool> TMaterialCache; struct ItemConstraint { @@ -205,6 +281,7 @@ struct ItemConstraint { TMaterialCache material_cache; +public: ItemConstraint() : weight(0), item_amount(0), item_count(0), item_inuse(0) {} int goalCount() { return config.ival(0); } @@ -238,11 +315,16 @@ struct ItemConstraint { } }; -/*******************************/ +/****************************** + * GLOBAL VARIABLES * + ******************************/ static bool enabled = false; static PersistentDataItem config; +static int last_tick_frame_count = 0; +static int last_frame_count = 0; + enum ConfigFlags { CF_ENABLED = 1 }; @@ -253,7 +335,9 @@ static TKnownJobs known_jobs; static std::vector pending_recover; static std::vector constraints; -/*******************************/ +/****************************** + * MISC FUNCTIONS * + ******************************/ static ProtectedJob *get_known(int id) { @@ -277,7 +361,9 @@ static void enumLiveJobs(std::map &rv) rv[p->item->id] = p->item; } -/*******************************/ +/****************************** + * STATE INITIALIZATION * + ******************************/ static void stop_protect(Core *c) { @@ -303,12 +389,12 @@ static void cleanup_state(Core *c) constraints.clear(); } -static bool check_lost_jobs(Core *c); +static void check_lost_jobs(Core *c, int ticks); static ItemConstraint *get_constraint(Core *c, const std::string &str, PersistentDataItem *cfg = NULL); static void start_protect(Core *c) { - check_lost_jobs(c); + check_lost_jobs(c, 0); if (!known_jobs.empty()) c->con.print("Protecting %d jobs.\n", known_jobs.size()); @@ -333,6 +419,9 @@ static void init_state(Core *c) c->getWorld()->DeletePersistentData(items[i]); } + last_tick_frame_count = world->frame_counter; + last_frame_count = world->frame_counter; + if (!enabled) return; @@ -354,7 +443,9 @@ static void enable_plugin(Core *c) start_protect(c); } -/*******************************/ +/****************************** + * JOB AUTO-RECOVERY * + ******************************/ static void forget_job(Core *c, ProtectedJob *pj) { @@ -364,6 +455,9 @@ static void forget_job(Core *c, ProtectedJob *pj) static bool recover_job(Core *c, ProtectedJob *pj) { + if (pj->isLive()) + return true; + // Check that the building exists pj->holder = df::building::find(pj->building_id); if (!pj->holder) @@ -396,31 +490,26 @@ static bool recover_job(Core *c, ProtectedJob *pj) // Create and link in the actual job structure df::job *recovered = cloneJobStruct(pj->job_copy); - recovered->flags.bits.repeat = true; - recovered->flags.bits.suspend = true; - if (!linkJobIntoWorld(recovered, false)) // reuse same id { deleteJobStruct(recovered); c->con.printerr("Inconsistency: job %d (%s) already in list.", pj->id, ENUM_KEY_STR(job_type, pj->job_copy->job_type)); - pj->live = true; return true; } pj->holder->jobs.push_back(recovered); // Done - pj->actual_job = recovered; - pj->live = true; + pj->recover(recovered); return true; } -static bool check_lost_jobs(Core *c) +static void check_lost_jobs(Core *c, int ticks) { - static int check = 1; - check++; + ProtectedJob::cur_tick_idx++; + if (ticks < 0) ticks = 0; df::job_list_link *p = world->job_list.next; for (; p; p = p->next) @@ -433,31 +522,25 @@ static bool check_lost_jobs(Core *c) if (!job->flags.bits.repeat) forget_job(c, pj); else - pj->check_idx = check; + pj->tick_job(job, ticks); } else if (job->flags.bits.repeat && isSupportedJob(job)) { pj = new ProtectedJob(job); assert(pj->holder); known_jobs[pj->id] = pj; - pj->check_idx = check; } } - bool any_lost = false; - for (TKnownJobs::const_iterator it = known_jobs.begin(); it != known_jobs.end(); ++it) { - if (it->second->check_idx == check || !it->second->live) + if (it->second->tick_idx == ProtectedJob::cur_tick_idx || + !it->second->isLive()) continue; - it->second->live = false; it->second->actual_job = NULL; pending_recover.push_back(it->second); - any_lost = true; } - - return any_lost; } static void update_job_data(Core *c) @@ -486,31 +569,39 @@ DFhackCExport command_result plugin_onupdate(Core* c) if (!enabled) return CR_OK; + // Every 5 frames check the jobs for disappearance static unsigned cnt = 0; + if ((++cnt % 5) != 0) + return CR_OK; + + check_lost_jobs(c, world->frame_counter - last_tick_frame_count); + last_tick_frame_count = world->frame_counter; + + // Proceed every in-game half-day, or when jobs to recover changed static unsigned last_rlen = 0; - cnt++; + bool check_time = (world->frame_counter - last_frame_count) >= DAY_TICKS/2; - if ((cnt % 5) == 0) + if (pending_recover.size() != last_rlen || check_time) { - check_lost_jobs(c); + recover_jobs(c); + last_rlen = pending_recover.size(); - if (pending_recover.size() != last_rlen || (cnt % 50) == 0) + // If the half-day passed, proceed to update + if (check_time) { - recover_jobs(c); - last_rlen = pending_recover.size(); + last_frame_count = world->frame_counter; - if ((cnt % 500) == 0) - { - update_job_data(c); - process_constraints(c); - } + update_job_data(c); + process_constraints(c); } } return CR_OK; } -/*******************************/ +/****************************** + * ITEM COUNT CONSTRAINT * + ******************************/ static ItemConstraint *get_constraint(Core *c, const std::string &str, PersistentDataItem *cfg) { @@ -592,6 +683,10 @@ static void delete_constraint(Core *c, ItemConstraint *cv) delete cv; } +/****************************** + * JOB-CONSTRAINT MAPPING * + ******************************/ + static void link_job_constraint(ProtectedJob *pj, df::item_type itype, int16_t isubtype, df::dfhack_material_category mat_mask, int16_t mat_type, int32_t mat_index) @@ -823,18 +918,27 @@ static void map_job_constraints(Core *c) { it->second->constraints.clear(); - if (!it->second->live) + if (!it->second->isLive()) continue; compute_job_outputs(c, it->second); } } +/****************************** + * ITEM-CONSTRAINT MAPPING * + ******************************/ + static bool itemNotEmpty(df::item *item) { for (unsigned i = 0; i < item->itemrefs.size(); i++) - if (strict_virtual_cast(item->itemrefs[i])) + { + df::general_ref *ref = item->itemrefs[i]; + if (strict_virtual_cast(ref)) return true; + if (strict_virtual_cast(ref)) + return true; + } return false; } @@ -869,10 +973,11 @@ static void map_job_items(Core *c) #define F(x) bad_flags.bits.x = true; F(dump); F(forbid); F(garbage_colect); F(hostile); F(on_fire); F(rotten); F(trader); - F(in_building); F(artifact1); + F(in_building); F(construction); F(artifact1); #undef F - std::vector &items = df::item::get_vector(); + std::vector &items = world->items.other[items_other_id::ANY_FREE]; + for (unsigned i = 0; i < items.size(); i++) { df::item *item = items[i]; @@ -930,12 +1035,18 @@ static void map_job_items(Core *c) constraints[i]->computeRequest(); } +/****************************** + * ITEM COUNT CONSTRAINT * + ******************************/ + +static std::string shortJobDescription(df::job *job); + static void update_jobs_by_constraints(Core *c) { for (TKnownJobs::const_iterator it = known_jobs.begin(); it != known_jobs.end(); ++it) { ProtectedJob *pj = it->second; - if (!pj->live || pj->constraints.empty()) + if (!pj->isLive() || pj->constraints.empty()) continue; int resume_weight = -1; @@ -949,19 +1060,22 @@ static void update_jobs_by_constraints(Core *c) suspend_weight = std::max(suspend_weight, pj->constraints[i]->weight); } - bool goal = pj->actual_job->flags.bits.suspend; + bool current = pj->isResumed(); + bool goal = current; if (resume_weight >= 0 && resume_weight >= suspend_weight) - goal = false; - else if (suspend_weight >= 0 && suspend_weight >= resume_weight) goal = true; + else if (suspend_weight >= 0 && suspend_weight >= resume_weight) + goal = false; - if (goal != pj->actual_job->flags.bits.suspend) + pj->set_resumed(goal); + + if (goal != current) { - pj->actual_job->flags.bits.suspend = goal; - c->con.print("%s job %d: %s\n", - (goal ? "Suspending" : "Resuming"), pj->id, - ENUM_KEY_STR(job_type, pj->actual_job->job_type)); + c->con.print("%s %s%s\n", + (goal ? "Resuming" : "Suspending"), + shortJobDescription(pj->actual_job).c_str(), + (!goal || pj->isActuallyResumed() ? "" : " (delayed)")); } } } @@ -976,13 +1090,49 @@ static void process_constraints(Core *c) update_jobs_by_constraints(c); } -/*******************************/ +/****************************** + * PRINTING AND THE COMMAND * + ******************************/ + +static std::string shortJobDescription(df::job *job) +{ + std::string rv = stl_sprintf("job %d: ", job->id); + + if (job->job_type != job_type::CustomReaction) + rv += ENUM_KEY_STR(job_type, job->job_type); + else + rv += job->reaction_name; + + MaterialInfo mat; + df::dfhack_material_category mat_mask; + guess_job_material(job, mat, mat_mask); + + if (mat.isValid()) + rv += " [" + mat.toString() + "]"; + else if (mat_mask.whole) + rv += " [" + bitfieldToString(mat_mask) + "]"; + + return rv; +} static void print_constraint(Core *c, ItemConstraint *cv, bool no_job = false, std::string prefix = "") { - c->con << prefix << "Constraint " << cv->config.val() << ": " - << (cv->goalByCount() ? "count " : "amount ") + Console::color_value color; + if (cv->request_resume) + color = Console::COLOR_GREEN; + else if (cv->request_suspend) + color = Console::COLOR_CYAN; + else + color = Console::COLOR_DARKGREY; + + c->con.color(color); + c->con << prefix << "Constraint " << flush; + c->con.color(Console::COLOR_GREY); + c->con << cv->config.val() << " " << flush; + c->con.color(color); + c->con << (cv->goalByCount() ? "count " : "amount ") << cv->goalCount() << " (gap " << cv->goalGap() << ")" << endl; + c->con.reset_color(); if (cv->item_count || cv->item_inuse) c->con << prefix << " items: amount " << cv->item_amount << "; " @@ -994,30 +1144,57 @@ static void print_constraint(Core *c, ItemConstraint *cv, bool no_job = false, s if (cv->jobs.empty()) c->con.printerr(" (no jobs)\n"); + std::vector unique_jobs; + std::vector unique_counts; + for (int i = 0; i < cv->jobs.size(); i++) { ProtectedJob *pj = cv->jobs[i]; + for (int j = 0; j < unique_jobs.size(); j++) + { + if (unique_jobs[j]->building_id == pj->building_id && + *unique_jobs[j]->actual_job == *pj->actual_job) + { + unique_counts[j]++; + goto next_job; + } + } + + unique_jobs.push_back(pj); + unique_counts.push_back(1); + next_job:; + } + + for (int i = 0; i < unique_jobs.size(); i++) + { + ProtectedJob *pj = unique_jobs[i]; df::job *job = pj->actual_job; - c->con << prefix << " job " << job->id << ": "; + std::string start = prefix + " " + shortJobDescription(job); - if (job->job_type != job_type::CustomReaction) - c->con << ENUM_KEY_STR(job_type, job->job_type); + if (!pj->isActuallyResumed()) + { + if (pj->want_resumed) + { + c->con.color(Console::COLOR_YELLOW); + c->con << start << " (delayed)" << endl; + } + else + { + c->con.color(Console::COLOR_BLUE); + c->con << start << " (suspended)" << endl; + } + } else - c->con << job->reaction_name; - - MaterialInfo mat; - df::dfhack_material_category mat_mask; - guess_job_material(job, mat, mat_mask); + { + c->con.color(Console::COLOR_GREEN); + c->con << start << endl; + } - if (mat.isValid()) - c->con << " [" << mat.toString() << "]"; - else if (mat_mask.whole) - c->con << " [" << bitfieldToString(mat_mask) << "]"; + c->con.reset_color(); - if (job->flags.bits.suspend) - c->con << " (suspended)"; - c->con << endl; + if (unique_counts[i] > 1) + c->con << prefix << " (" << unique_counts[i] << " copies)" << endl; } } @@ -1026,7 +1203,7 @@ static void print_job(Core *c, ProtectedJob *pj) if (!pj) return; - printJobDetails(c, pj->job_copy); + printJobDetails(c, pj->isLive() ? pj->actual_job : pj->job_copy); for (int i = 0; i < pj->constraints.size(); i++) print_constraint(c, pj->constraints[i], true, " "); @@ -1037,7 +1214,7 @@ static command_result workflow_cmd(Core *c, vector & parameters) CoreSuspender suspend(c); if (enabled) { - check_lost_jobs(c); + check_lost_jobs(c, 0); recover_jobs(c); update_job_data(c); map_job_constraints(c); @@ -1080,7 +1257,7 @@ static command_result workflow_cmd(Core *c, vector & parameters) stop_protect(c); return CR_OK; } - else if (cmd == "limit" || cmd == "limit-count") + else if (cmd == "count" || cmd == "amount") { if (!enabled) enable_plugin(c); @@ -1099,7 +1276,7 @@ static command_result workflow_cmd(Core *c, vector & parameters) else { for (TKnownJobs::iterator it = known_jobs.begin(); it != known_jobs.end(); ++it) - if (it->second->live) + if (it->second->isLive()) print_job(c, it->second); } @@ -1128,7 +1305,7 @@ static command_result workflow_cmd(Core *c, vector & parameters) return CR_OK; } - else if (cmd == "limit" || cmd == "limit-count") + else if (cmd == "count" || cmd == "amount") { if (parameters.size() < 3) return CR_WRONG_USAGE; @@ -1143,15 +1320,14 @@ static command_result workflow_cmd(Core *c, vector & parameters) if (!icv) return CR_FAILURE; - icv->setGoalByCount(cmd == "limit-count"); + icv->setGoalByCount(cmd == "count"); icv->setGoalCount(limit); if (parameters.size() >= 4) icv->setGoalGap(atoi(parameters[3].c_str())); else icv->setGoalGap(-1); - map_job_constraints(c); - map_job_items(c); + process_constraints(c); print_constraint(c, icv); return CR_OK; }