/** * Parse a build order * @param string $buildOrder */ public function __construct($buildOrder) { Logger::enter("Parser::_construct"); $this->checkpoints = array(); $this->options = array(); $this->jobs = array(); // parse line-by-line $lines = preg_split("/([\n])/", $buildOrder); foreach ($lines as $lineNumber => $line) { $this->read($lineNumber, trim($line)); } if (count($this->jobs) == 0) { throw_error("No commands found in build order!", "Your build order is empty. For a quick demonstration of the workings of this calculator, click one of the examples listed under <i>Examples of complete build orders</i>."); } Logger::leave("Parser::_construct"); }
/** * Process a single job, update the timeline accordingly, and handle all * job-specific tasks. * @global Product $SpawnLarvae * @global Product $Warpgate * @global Product $ScoutingWorker * @param Job $job * @param bool $intheFuture */ public function process($job, $intheFuture = false) { global $SpawnLarvae, $Warpgate, $ScoutingWorker; Logger::enter("Timeline::process"); if ($this->debug || Hatcheries::$debug) { tracemsg("Timeline::process(" . $job . ", " . ($intheFuture ? "true" : "false") . ")"); } // reset time completed $job->timeCompleted = INF; // handle mutations up to job start foreach ($job->mutations() as $mutation) { if ($mutation->time < $job->timeStarted) { $this->income->splice($mutation); } } // handle checkpoints up to job start if (!$intheFuture) { $this->processCheckpoints($job->timeStarted); } // update all if (!$intheFuture) { $this->update($job->timeStarted); } // expend resources $this->income->expend($job->mineralCost(), $job->gasCost()); // when is job completed $job->timeCompleted = $job->timeStarted + $job->duration(); // refund resources $this->income->expend(-$job->gasRefund(), 0); $this->income->expend(-$job->mineralRefund(), 0); // use production queues list($queueTypesExpended, $expendAll) = $job->queueTypesExpended(); if ($queueTypesExpended !== null) { list($job->timeCompleted, $queues) = $this->queue($job); } // use energy if ($job->energyCost() > 0) { $this->spellcasters->expend($job->spellcasterTypeExpended(), $job->energyCost(), $job->timeStarted, $job->tagsRequired); } // special case: build is complete in 5 seconds when using a warpgate if (isset($queues) && count($queues) == 1) { if ($queues[0]->structure == $Warpgate) { $job->timeCompleted = $job->timeStarted + 5; } } // use larva if ($job->larvaCost() > 0) { $this->hatcheries->expend($job->timeStarted, $job->larvaCost(), $job->tagsRequired); } // new products if ($job->productsCreated() !== null) { foreach ($job->productsCreated() as $product) { if ($product !== null) { // spawn larvae if ($product->uid == $SpawnLarvae->uid) { $this->hatcheries->vomit($job->timeStarted); } // new hatchery if ($product->type & Base && $product->type & Zerg) { $this->hatcheries->add(new Hatchery($job->timeCompleted, 1, $job->tag)); } // new spellcaster if ($product->type & Spellcaster) { $this->spellcasters->add(new Spellcaster($product, $job->timeCompleted, $job->tag)); } // new farm if ($product->supplyCapacity > 0) { //tracemsg("Adding farm ". $product ." at ". simple_time($job->timeCompleted)); $this->farms->add(new Farm($job->timeCompleted, $product->supplyCapacity)); } } } } // destroy products if ($job->productsDestroyed() !== null) { foreach ($job->productsDestroyed() as $product) { // destroy spellcaster if ($product->type & Spellcaster) { $this->spellcasters->remove($product, $job->timeCompleted); } // destroy farm if ($product->supplyCapacity > 0) { $this->farms->remove($product->supplyCapacity, $job->timeCompleted); } } } // process mutations foreach ($job->mutations() as $mutation) { if ($mutation->time >= $job->timeStarted) { $this->income->splice($mutation); } } // add or morph production queues $queueTypesCreated = $job->queueTypesCreated(); if ($queueTypesCreated !== null) { if (isset($queues) && $job->morph()) { $this->queues->morph($queues, $job->timeStarted, $queueTypesCreated, $job->timeCompleted); } else { foreach ($queueTypesCreated as $queueType) { $this->queues->add(new ProductionQueue($queueType, $job->timeCompleted, $job->tag)); } } } // create event if ($intheFuture) { $this->addCheckpoint(new Checkpoint($job->description(), $job->timeStarted, $job->timeCompleted)); } else { $this->log($job->description(), $job->timeStarted, $job->timeCompleted); } // update supply count if ($this->debug) { tracemsg("Timeline::process(), supply count = " . $this->supplyCount . " + " . $job->supplyCost(false) . "."); } $this->supplyCount += $job->supplyCost(false); Logger::leave("Timeline::process"); }
/** * 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; }
// but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with sc2calc.org. If not, see <http://www.gnu.org/licenses/>. /** * @package sc2calc.org * @copyright 2010 Jasper Abraham Visser * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $logtime = microtime(true); set_time_limit(1); require "include.php"; require "logger.php"; Logger::enter("update.php, require()"); require "farm.php"; require "hatchery.php"; require "income.php"; require "job.php"; require "mutation.php"; require "parser.php"; require "product.php"; require "queue.php"; require "scheduler.php"; require "spellcaster.php"; require "timeline.php"; Logger::leave("update.php, require()"); // parse input $parser = new Parser(_GET("buildOrder")); $unscheduledJobs = $parser->jobs;