/** * Calculate time when job would be completed, and expend production queues. * @global Product $ChronoBoost * @global Product $Nexus * @global Product $Warpgate * @param Job $job * @param float $time * @param bool $tentative If true, chronoboosts will not be logged. Use this * to perform dry runs of the queue use. * @return array(int,array) First element is time job would be completed, * second element is list of production queues used. */ public function queue($job, $time = null, $tentative = false) { global $ChronoBoost, $Nexus, $Warpgate; if ($this->debug) { tracemsg("Timeline::queue(" . $job . ", " . simple_time($time) . ", " . ($tentative ? "true" : "false") . ")"); } if ($time === null) { $time = $job->timeStarted; } if ($this->debug) { tracemsg("Timeline::queue(), job starts at " . simple_time($time)); } // choose queues list($queueTypesExpended, $expendAll) = $job->queueTypesExpended(); if ($queueTypesExpended !== null) { $queues = $this->queues->choose($time, $queueTypesExpended, $expendAll, $job->tagsRequired); } // build time $buildTime = $job->duration(); if (isset($queues) && count($queues) == 1 && $queues[0]->structure == $Warpgate) { $buildTime -= WARPGATE_QUEUE_REDUCTION; } // previous chrono boost overlaps this job if (isset($queues) && count($queues) == 1 && $queues[0]->chronoboosted + $ChronoBoost->timeCost > $time) { $boostTime = $queues[0]->chronoboosted; // calculate overlap with job $overlapStart = max($boostTime, $time); $overlapEnd = min($boostTime + $ChronoBoost->timeCost * CHRONO_BOOST_RATE, $time + $buildTime); $overlap = max(0, $overlapEnd - $overlapStart); // reduce build time $buildTime -= $overlap - $overlap / CHRONO_BOOST_RATE; } // chrono boosts if (isset($queues) && count($queues) == 1) { // process chronoboosts in an alternate reality $spellcasters = clone $this->spellcasters; for ($i = 0; $i < $job->chronoboost; $i++) { // start time of chrono boost $boostTime = max($queues[0]->chronoboosted + $ChronoBoost->timeCost, $spellcasters->when($Nexus, $ChronoBoost->energyCost)); if ($boostTime < $time + $buildTime) { $boostTime = max($boostTime, $time + CHRONO_BOOST_HUMAN_DELAY); // calculate overlap with job $overlapStart = max($boostTime, $time); $overlapEnd = min($boostTime + $ChronoBoost->timeCost * CHRONO_BOOST_RATE, $time + $buildTime); $overlap = max(0, $overlapEnd - $overlapStart); // reduce build time $buildTime -= $overlap - $overlap / CHRONO_BOOST_RATE; // expend spellcasters $spellcasters->update($boostTime); $spellcasters->expend($Nexus, $ChronoBoost->energyCost, $boostTime); // log chronoboost & reserve energy if (!$tentative) { $this->addCheckpoint(new Checkpoint("<em>CB: " . $job->description() . "</em>", $boostTime, $boostTime + $ChronoBoost->timeCost)); $spellcaster = $this->spellcasters->reserve($Nexus, $ChronoBoost->energyCost, $boostTime); } // queue is now chrono boosted $queues[0]->chronoboosted = $boostTime; } } } // build complete $completed = $time + $buildTime; // queue is now unavailable if (isset($queues)) { foreach ($queues as $queue) { $queue->busy($time, $completed, $job->busiesQueues()); } return array($completed, $queues); } else { return array($completed, null); } }
/** * Remove the spellcaster of the given type with the least energy. * @param Product $casterType * @param float $time Time of removal */ public function remove($casterType, $time) { if (self::$debug) { tracemsg("Spellcasters::remove(" . $casterType . ", " . simple_time($time) . ")"); } // choose a spellcaster to remove foreach ($this->select($casterType, $tagsRequired) as $spellcaster) { if ($spellcaster->created <= $time) { if (!isset($candidate)) { $candidate = $spellcaster; } elseif ($spellcaster->energy() < $candidate->energy()) { $candidate = $spellcaster; } } } // if no such spellcaster exists, throw an error if (!isset($candidate)) { throw_error("No spellcaster of type <i>" . $casterType->name . "</i> could be removed.", "This error message should not occur. Please report this message with your build order on the thread linked at bottom of the page."); } // mark it as destroyed if (self::$debug) { tracemsg("Spellcasters::remove(), chosen " . $candidate->casterType . " created at " . simple_time($candidate->created)); } $candidate->destroyed = $time; }
/** * Calculate time when another vomit can be queued on any hatchery. * @return float */ public function whenVomit() { $time = INF; foreach ($this->_hatcheries as $hatchery) { $time = min($time, $hatchery->whenVomit()); } if (self::$debug) { tracemsg(($this->_isClone ? "lon " : "") . "Hatcheries::whenVomit(), returns " . simple_time($time)); } return $time; }
/** * Schedule as a floating job that can be squeezed in without delaying the * fixed job. If there is supply gap before the fixed job, a floating * jobs is scheduled as needed to bridge the gap, possibly delaying the * fixed job. * @param Job $fixedJob Fixed job * @param bool $recurring If set, consider either only recurring or * non-recurring jobs. * @return bool True, if a job could be squeezed in. */ private function squeeze($fixedJob, $recurring = null) { Logger::enter("Scheduler::squeeze"); if (self::$debug) { tracemsg("Scheduler::squeeze(" . $fixedJob . ", " . ($recurring === null ? "null" : ($recurring ? "true" : "false")) . ")"); } // squeezing is mandatory if the fixed job is unavailable $mandatory = $fixedJob->availability->status != Availability::Available; if (self::$debug) { tracemsg("Scheduler::squeeze(), squeezing is " . ($mandatory ? "" : "not ") . "mandatory!"); } // choose candidates if ($recurring === null) { $candidates = array_merge($this->_floatingJobs, $this->_recurringJobs); } elseif ($recurring) { $candidates = $this->_recurringJobs; } else { $candidates = $this->_floatingJobs; } if (self::$debug) { tracemsg("Scheduler::squeeze(), choosing from " . count($candidates) . " candidates!"); } // ignore recurring candidates that build the same product as the fixed job if (!$mandatory && $fixedJob->productBuilt() !== null) { foreach ($candidates as $key => $job) { if ($job->recurring && $job->productBuilt() !== null && $job->productBuilt()->uid == $fixedJob->productBuilt()->uid) { if (self::$debug) { tracemsg("Scheduler::squeeze(), eliminating " . $job . ", which builds the same product as " . $fixedJob . "."); } unset($candidates[$key]); } } } // ignore candidates that are not available before fixed job starts // if mandatory, instead ignore candidates that are not available ever foreach ($candidates as $key => $job) { $this->_timeline->calculate($job, $this->_scheduledJobs); if (!$mandatory && $job->timeStarted > $fixedJob->timeStarted) { if (self::$debug) { tracemsg("Scheduler::squeeze(), eliminating " . $job . ", which is available at " . simple_time($job->timeStarted) . ", but fixed job starts at " . simple_time($fixedJob->timeStarted)); } unset($candidates[$key]); } elseif ($mandatory && $job->timeStarted === INF) { if (self::$debug) { tracemsg("Scheduler::squeeze(), eliminating " . $job . ", which is unavailable because " . $job->availability); } unset($candidates[$key]); } } // ignore jobs that affect supply the wrong way if (isset($fixedJob->triggerSupply)) { $supplyGap = $fixedJob->triggerSupply - $this->_timeline->supplyCount; if (self::$debug) { tracemsg("Scheduler::squeeze(), supply gap is " . $supplyGap . ", current supply count is " . $this->_timeline->supplyCount . ", fixed job is triggered at " . $fixedJob->triggerSupply); } foreach ($candidates as $key => $job) { if ($supplyGap == 0 && $job->supplyCost(true) != 0) { if (self::$debug) { tracemsg("Scheduler::squeeze(), eliminating <i>" . $job . "</i>; Supply gap is " . $supplyGap . ", job's supply cost is " . $job->supplyCost(true)); } unset($candidates[$key]); } elseif ($supplyGap > 0 && ($job->supplyCost(true) > $supplyGap || $job->supplyCost(true) < 0)) { if (self::$debug) { tracemsg("Scheduler::squeeze(), #3 Eliminating " . $job); } unset($candidates[$key]); } elseif ($supplyGap < 0 && ($job->supplyCost(true) < $supplyGap || $job->supplyCost(true) > 0)) { if (self::$debug) { tracemsg("Scheduler::squeeze(), #4 Eliminating " . $job); } unset($candidates[$key]); } } } // if not mandatory, ignore jobs that exceed surplus minerals or gas if (!$mandatory) { foreach ($candidates as $key => $job) { // always allow jobs that don't cost resources if ($job->mineralCost() == 0 && $job->gasCost() == 0) { continue; } // the job affects income $mutations = $job->mutations(); $mutations->sort(); if (count($mutations) > 0) { // calculate job start & complete time $jobComplete = $this->_timeline->whenComplete($job); // set up alternate reality income $income = clone $this->_timeline->income; foreach ($mutations as $mutation) { $income->splice($mutation); } // the job does not affect income } else { $income = $this->_timeline->income; } // if surplus is not great enough list($gasSurplus, $mineralSurplus) = $income->surplus($fixedJob->timeStarted); if (round($gasSurplus) < $job->gasCost() + $fixedJob->gasCost()) { if (self::$debug) { tracemsg("Scheduler::squeeze(), eliminating " . $job . ". Gas needed for both jobs is " . ($job->gasCost() + $fixedJob->gasCost()) . ", gas surplus is " . $gasSurplus . "."); } unset($candidates[$key]); } if (round($mineralSurplus) < $job->mineralCost() + $fixedJob->mineralCost()) { if (self::$debug) { tracemsg("Scheduler::squeeze(), eliminating " . $job . ". Mineral needed for both jobs is " . ($job->mineralCost() + $fixedJob->mineralCost()) . ", mineral surplus is " . $mineralSurplus . "."); } unset($candidates[$key]); } } } // if not mandatory, ignore jobs whose larvae, production queue // or spellcaster usage would stall fixed job if (!$mandatory) { foreach ($candidates as $key => $job) { if (!$this->_timeline->canAccommodate($job, $fixedJob)) { if (self::$debug) { tracemsg("Scheduler::squeeze(), #16 Eliminating " . $job); } unset($candidates[$key]); } } } // ignore candidates that exceed supply gap $supplyGap = $this->supplyGap(); foreach ($candidates as $key => $job) { if ($supplyGap >= 0 && $job->supplyCost(true) > $supplyGap) { if (self::$debug) { tracemsg("Scheduler::squeeze(), #9 Eliminating " . $job); } unset($candidates[$key]); } } // ignore jobs that cause fixed job to exceed supply capacity foreach ($candidates as $job) { if ($job->supplyCost(true) > 0) { // how much supply capacity is needed $supplyNeeded = $this->_timeline->supplyCount + $fixedJob->supplyCost(true) + $job->supplyCost(true); // discard candidate if supply capacity is not available $time = $this->_timeline->farms->when($supplyNeeded); if (!$mandatory && $time > $fixedJob->timeStarted) { if (self::$debug) { tracemsg("Scheduler::squeeze(), #14 Eliminating " . $job); } unset($candidates[$key]); } elseif ($mandatory && $time === INF) { if (self::$debug) { tracemsg("Scheduler::squeeze(), #15 Eliminating " . $job); } unset($candidates[$key]); } } } // no floating jobs are available if (count($candidates) == 0) { if (self::$debug) { tracemsg("Scheduler::squeeze(), all jobs were eliminated!"); } // if mandatory, throw an error if ($mandatory) { $this->reportUnavailable(array($fixedJob)); } Logger::leave("Scheduler::squeeze"); return false; } // choose earliest available job $job = $this->earliest($candidates); // process floating job if (self::$debug) { $report = ""; foreach ($candidates as $candidate) { $report .= (isset($notFirst) ? ", " : "") . $candidate . "(" . simple_time($candidate->timeStarted) . ")"; $notFirst = true; } tracemsg("Scheduler::squeeze(), remaining candidates are " . $report); } if (self::$debug) { tracemsg("Scheduler::squeeze(), chosen " . $job); } $this->process($job); // reschedule, if recurring if ($job->recurring) { $this->_recurringJobs[] = clone $job; } Logger::leave("Scheduler::squeeze"); return true; }
/** * Calculate when the given queue types are available. * @param array $queueTypes * @param bool $expendsAll * @param array $tagsRequired * @return float */ public function when($queueTypes, $expendsAll, $tagsRequired = null) { if (!isset($queueTypes) || count($queueTypes) == 0) { return $this->_lastUpdated; } // when are production queues available if (self::$debug) { tracemsg("ProductionQueues::when(), Looking for available queues"); } $queuesAvailable = $expendsAll ? 0 : INF; $unavailableQueues = array(); foreach ($queueTypes as $expend) { $queues = $this->select($expend, $tagsRequired); // when is production queue of this type available $queueAvailable = INF; foreach ($queues as $queue) { $queueAvailable = min($queueAvailable, $queue->available); } if (self::$debug) { tracemsg("ProductionQueues::when(), " . count($queues) . " Queues of type " . $expend . ", earliest available at " . simple_time($queueAvailable)); } if ($queueAvailable === INF) { $unavailableQueues[] = $expend; } if ($expendsAll) { $queuesAvailable = max($queuesAvailable, $queueAvailable); } else { $queuesAvailable = min($queuesAvailable, $queueAvailable); } } if (self::$debug) { tracemsg("ProductionQueues::when(), all queues available at " . simple_time($queuesAvailable)); } // some or all queues are unavailable if ($queuesAvailable === INF) { if (self::$debug) { tracemsg("ProductionQueues::when(), no production queue of type <i>" . implode("</i>, <i>", $unavailableQueues) . "</i> is available."); } return array(INF, $unavailableQueues); } return array(max($this->_lastUpdated, $queuesAvailable), null); }
/** * Update time slots up to the given time. * @param float $time */ public function update($time) { if ($this->debug) { tracemsg("IncomeSlots::update(" . simple_time($time) . ")"); } foreach ($this->_slots as $slot) { list($gasSurplus, $mineralSurplus) = $slot->surplus($time); if ($this->debug) { tracemsg("IncomeSlots::update(), we gain " . $mineralSurplus . " minerals and " . $gasSurplus . " gas."); } if ($this->debug) { tracemsg("from slot starting at " . $slot->startTime . " and ending at " . $slot->endTime); } $this->_gasStored += $gasSurplus; $this->_mineralStored += $mineralSurplus; $slot->update($time); if ($this->debug) { tracemsg("IncomeSlots::update(), we got " . $this->_mineralStored . " minerals and " . $this->_gasStored . " gas."); } } $this->_lastUpdated = $time; }
.</p> <?php $timeline->queues->timeEnds = $timeEnds; echo (string) $timeline->queues; // render income ?> <h3>Income</h3> <p>This table shows the income generated by your workers. The timeline is divided into timeslots, each corresponding with a different distribution of workers. A new timeslot starts when a worker is created, assigned to a job, or transferred to a new base or geyser.</p> <p>You have mined a total of <em><?php echo simple_round($timeline->income->totalMineral($timeEnds)); ?> </em> minerals and <em><?php echo simple_round($timeline->income->totalGas($timeEnds)); ?> </em> gas at <?php echo simple_time($timeEnds); ?> .</p> <?php echo (string) $timeline->income; // render hatcheries if ($race == Zerg) { ?> <h3>Larvae generated</h3> <p>This table shows the larvae generated by your hatcheries.</p> <?php $timeline->hatcheries->update($timeEnds); echo (string) $timeline->hatcheries; } // render function call log if ($debugFlags & 16) {