/** * 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 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); } }
/** * 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); }
/** * Calculate when the given amount of resources is available. * @param float $mineralNeeded * @param float $gasNeeded * @return float */ public function when($mineralNeeded, $gasNeeded) { if ($this->debug) { tracemsg("IncomeSlots::when(" . $mineralNeeded . ", " . $gasNeeded . ")"); } // how much is needed $mineralNeeded -= $this->_mineralStored; $gasNeeded -= $this->_gasStored; // calculate breaking points foreach ($this->_slots as $slot) { list($mineralTimeInSlot, $gasTimeInSlot) = $slot->when($mineralNeeded, $gasNeeded); if ($mineralTimeInSlot === INF) { $mineralNeeded -= $slot->mineralRate() * $slot->duration(); } elseif (!isset($mineralTime)) { $mineralTime = $mineralTimeInSlot; } if ($gasTimeInSlot === INF) { $gasNeeded -= $slot->gasRate() * $slot->duration(); } elseif (!isset($gasTime)) { $gasTime = $gasTimeInSlot; } if (isset($gasTime) && isset($mineralTime)) { break; } } if (!isset($gasTime)) { $gasTime = INF; } if (!isset($mineralTime)) { $mineralTime = INF; } if ($this->debug) { tracemsg("IncomeSlots::when(), mineralTime=" . $mineralTime . ", gasTime=" . $gasTime); } if ($this->debug) { tracemsg("IncomeSlots::when(), storedMineral=" . $this->_mineralStored . ", storedGas=" . $this->_gasStored); } return max($mineralTime, $gasTime); }