Skip to content

Latest commit

 

History

History
91 lines (62 loc) · 31.9 KB

README.md

File metadata and controls

91 lines (62 loc) · 31.9 KB

Museum Source Code

This is the source directory for the museum. This document explains how to make changes. To download and use the package, go up a level.

Making changes

Modifying the code requires the enchanced editor at https://d0sboots.github.io/perfect-tower, because it :imports some scripts to use as macro libraries.

The editor has a source-import feature, used the same way you import scripts. The following will set up a new workspace for the museum:

{"workspaces":{"museum2":[["museum_macros","; Standardized (package) naming across all the scripts\n#script(name) D0S.Museum v4.7:{name}\n\n; Keybindings. You can edit these here, or edit them in the scripts directly\n; (but it will be much more error-prone).\n#up w\n#down s\n#start m\n\n; Name of the budget variable, which begins the script-hiding block.\n#budget \"<size=0>mb**\"\n\n; Turn an element string into an index. For our purpose, all the indexes\n; need to be multiplied by two, so we look for the index of the first two\n; characters of the element.\n; The \"offset\" string allows us to concatenate on a string and thus add a\n; constant to the result.\n#element_to_index_base(ele, offset) index({offset} . \"lielfidanaaiwaeaun\", sub({ele}, 0, 2), 0)\n\n; Macro for determining the number of tiers we can boost, given our budget and\n; stones of \"in_tier\".\n; We allocate 1% of budget to each stone. Based on the tier of the stones we\n; can buy, they cost 2000 * 18^in_tier / 18 each. (We ignore the extra cost\n; of universal stones.) This determines how many stones we can buy, and thus,\n; the max tier.\n; There is an extra division by 9 and a floor. This means that if the level can't\n; be raised by at least 2 levels, the quantity in the log will be rounded down to 0,\n; and thus the log will result in -infinity. We add back the 2 levels on the outside.\n#up_tiers(in_tier) floor(gdg({budget}) / (100. * 2000. * 9. / 18.) / (18. ^ ({in_tier}))) // 3. + 2.\n\n; The top tier that can be achieved, given input stones of \"in_tier\". Capped\n; either at +11 levels, or by the budget function of up_tiers.\n; If we can't reach +2 levels, up_tiers returns -inf, which will result in -1.\n; from this function.\n#top_tier(in_tier) max(-1., min(50., min(11., {up_tiers({in_tier})}) + ({in_tier})))\n"],["Buy",":import museum_macros\n:name {script(Buy)}\n\n; This isn't included in this subdirectory, but comes from the factory/ directory.\n:import worker_storage_lib\n\n; This script is responsible for buying all stones.\n;\n; It also contains initialization logic for variables. They must be set in the\n; proper order, so that the remain hidden inside our <size=0> block. This also\n; reads/writes the budget from worker storage, and increment/decrements it\n; if the right keys are pressed.\n\nkey.{up}()\nkey.{down}()\n\nisopen(\"museum\")\n\n:global string museum_status\n:global string offer_tiers\n:global int museum_tier\n:global int target_tier\n:global int museum_pos\n\n:global int turbo.register\n:global int turbo.cycles\n\n:local string local_elem\n:local int local_buy_amount\n:local double worker_val\n\n; If we're being called from Combine, we're a buyer.\n; Otherwise, if we're being invoked due to key-impulse, but the combiner is\n; currently running, abort. We don't want to change the budget in the middle\n; of things.\ngoto(if(\\\n  contains(\"{script(Combine)}\", impulse()),\\\n  buy,\\\n  if(\\\n    contains(\"key.{up}|key.{down}\", impulse()) &&\\\n      contains(museum_status, \"Combining\"),\\\n    end,\\\n    init\\\n  )\\\n))\n\ninit:\n\n; We do *not* direct-start turbo, because we want to detect broken turbo installs.\n; In particular, if someone has turbo v2.1 still installed, that would break, but in a way\n; that's not easy to programatically detect if we direct-start at this point.\nexecutesync(\"TE2.2:start\")\n\n; Use worker_storage_lib to find a worker_slot to use for permanent storage.\n#prefix [museum]\n:local int worker_slot\n{worker_lib_line_1({prefix})}\n{worker_lib_line_2}\n\n; Fetch the budget from the chosen worker. The s2d fallback handles the case\n; where we're allocating new storage, as well.\nworker_val = s2d(sub(worker.name(worker_slot), {len({prefix})}, 99), 1e11)\n\n; Ensure this value is set before we start hiding variables.\n; If we're launching on wakeup(), the other turbo variables will get set\n; in time, but this one might not.\nturbo.cycles = turbo.cycles\n\n; If we're incrementing or decrementing the budget, round it to a fixed\n; amount. This allows users to set it to a specific amount by modifying the\n; worker if they wish, but we'll always get clean set-points when changing\n; via the UI.\n; The lower bound is 1e5, since at that point you can't buy any stones\n; (according to our budget).\n; Also, set budget to -1 if there are no worker slots available. This is our\n; signal to Main, so that it can display an appropriate error message.\ngds({budget}, if(\\\n  worker_slot == 200,\\\n  -1.,\\\n  if(\\\n    contains(impulse(), \"key.\"),\\\n    (10. ^ 0.5) ^ max(10., min(560.,\\\n      round(worker_val // (10. ^ 0.5)) +\\\n      if(contains(impulse(), \"key.{up}\"), 1., -1.)\\\n    )),\\\n    worker_val\\\n  )\\\n))\n; Save the budget back. There's always enough room for the full value.\nworker.setName(\\\n  worker_slot - if(worker_slot < 100, 0, 100),\\\n  \"{prefix}\" . gdg({budget})\\\n)\n\n; Set other variables that need to happen before we close the script block.\n\n; If target_tier is -2, we're being invoked because start was pressed: Either\n; during the timer countdown, or to abort a running combine. Either way,\n; museum_tier needs to be set to 0 to signal no countdown.\n;\n; Otherwise, we leave it unchanged, which will usually mean the countdown remains\n; in effect.\nmuseum_tier = if(target_tier == -2, 0, museum_tier)\nmuseum_pos = -1\n\n; Signal Main to wake up. We only want to override target_tier if it's idle,\n; i.e. set to 0 - other situations show that it's in use, and notifying about\n; changing budget is the lowest priority.\ntarget_tier = if(target_tier == 0, -3, target_tier)\n\n; Time for more ugly math! When we set preferred tier to \"pref\", we get stones in\n; the range [pref-10, pref], at uniform. We want our best stone to be in that\n; range - the one that we can *just* afford to do +11 levels from. Stones smaller\n; than that fall off at a rate of 1 tier/level, and stones higher fall off at a\n; rate of (18 log 3 - 1), or ~1.63 tiers/level. To maximize the potential of the\n; range, we want the top and bottom of the range to have equally high max_tiers,\n; so max_tier(pref - 10.5) = max_tier(pref + .5). (The halves make the range of\n; size 11, which is needed to properly match the actual integer-sized range.)\n;\n; Substituting gives pref - 10.5 + 11 = log_3(budget / (2000*100/18) / 18^(pref+.5)) + pref+.5,\n; 0 = log_3(budget / K) - (pref + .5) * log_3(18),\n; pref + .5 = log_3(budget / K) / log_3(18),\n; pref = log_18(budget / K) - .5,\n; pref = log_18(budget / (2000*100/18) / 18^.5),\n; pref = log_18(budget / (2000*100/18^.5))\n;\n; The result ends up being intuitive - since the range of stones spans 11 tiers, and\n; our uptiers is also +11 tiers, we want to match the preferred tier to the point where\n; we can just barely afford to buy the stone.\nmuseum.setPreferredTier(max(1, min(50, d2i((gdg({budget}) / (2000. * 100. / (18. ^ 0.5))) // 18.))))\n\n; We don't care about waiting for the frame break, so manipulate the value\n; directly instead of using \"turbo stop\".\n; This also gives some (useful) extra turbo frames to our caller, Main.\nturbo.register -= 1\n\nbuy:\n; If we're only upgrading 5 or fewer levels, we buy one-at-a-time to avoid\n; overbuying. The number of operations is so small that it won't be slow.\n; This actually maxes out on speed at +7 levels, because at that point we are\n; buying 6 each cycle, which matches the amount the combines use.\nlocal_buy_amount = max(1, (target_tier - museum_tier - 5) * 3)\n\n; If we're a buyer, use the stone in inventory.\n; Otherwise, buy nothing until we can exit normally.\nlocal_elem = if(\\\n  contains(\"{script(Combine)}\", impulse()),\\\n  element(\"inventory\", 0),\\\n  \"\"\\\n)\n\n; Because of turbo exec, this script will keep running every cycle, until\n; the frame pause. At that point, it will exit, and we rely on Combine to\n; restart it.\n;\n; Everything is very carefully engineered to make this expression as minimal\n; as possible. It's worth spending extra cycles in set-up in order to remove\n; a single node from this expression.\nmuseum.buyTier(local_elem, museum_tier, local_buy_amount)\n\nend:\n"],["Combine",":import museum_macros\n:name {script(Combine)}\n\n; This script is responsible for actually combining gems.\n;\n; That's true in two senses: It both has the actual call to Combine(), and\n; also the logic immediately surrounding it that runs the combines and checks\n; to see if the loop is done.\n\n:global int museum_tier\n:global int target_tier\n:global int museum_pos\n\n:global int turbo.cycles.max\n:global int turbo.register\n\n:local double end_time\n\n; Stop our parent. If we're invoking ourselves, this is a wasted cycle.\nstop(\"{script(Calculate)}\")\n\n; If we're invoking ourselves, we're a dedicated Combine()er.\n; Otherwise, check to see if we've already reached our goal.\n; If we have, we can exit early, otherwise fall through to the main logic.\ngoto(if(\\\n  contains(impulse(), \"{script(Combine)}\"),\\\n  combine,\\\n  if(tier(\"inventory\", 0) >= target_tier, done, restart_loop)\\\n))\n\n; Keep restarting the buy and combine scripts whenever the turbo frame ends.\n; They rely on the 1-cycle-end-of-script-loop property of turbo to function,\n; so we also keep the cycle max up for efficiency.\nrestart_loop:\n\n; We start 3 copies of combine, because each combine only combines 3 gems.\n; (As opposed to when you do it manually: then it does 2 combines, once on\n; mousedown and one on mouseup.) 3 is roughly optimal: there are very few\n; positions the inventory can be in that don't support 3 combines between buys,\n; whereas there are significantly more for 4. And combine is one of the most\n; time-expensive operations in the loop, so we don't want to run it pointlessly.\nexecute(\"{script(Combine)}\")\nexecute(\"{script(Combine)}\")\nexecute(\"{script(Combine)}\")\n\n; Start the buyer. By running this after the combiners, we'll always\n; end each cycle with a full inventory, which is important for the check below.\nexecute(\"{script(Buy)}\")\n\n; Extend the max duration. There is a possible race condition where we might\n; already be exiting turbo due to being at too many cycles; this is dealt with\n; (and discussed more) in Calculate. We can never be in the race condition\n; internally, because it does not take nearly 200 cycles to get from the\n; \"turbo stop\" to this line.\n;\n; Normally we would calculate this based on cycles and cycles.max, but here\n; we're just setting it to the maximum that turbo supports. We rely on our own\n; fps-based throttling, instead. If we *do* manage to go the whole 50000 cycles\n; in less than 200ms, there's an interesting timing effect where we will execute\n; TE2.2:stop *exactly* as the next turbo cycle is beginning, leading to a wasted\n; turbo cycle and drastically slower execution.\nturbo.cycles.max = 50000\n\n; Calculate a time to end the loop at. This is 1/5 of a second in the future,\n; so it should reliably be 5 FPS.\nend_time = now() + 10000000. / 5.\n\n; The core condition to wait for. This condition needs to be as minimal as\n; possible, because it is draining CPU time away from the important combine/buy\n; tasks every cycle. (So is the turbo exec machinery, but we can't help that.)\nwaituntil(tier(\"inventory\", 0) >= target_tier || now() > end_time)\n\n; If we are done, stop the buyers ASAP, to avoid wasting money. The condition\n; here relies on the condition above for correctness, but is simpler.\n; (Technically there's an edge case if it finishes right at the end, but the\n; number of cycles required means that won't happen.)\nstop(if(now() > end_time, \"\", \"{script(Buy)}\"))\n\n; We restart turbo by changing the variable directly, so that there isn't a\n; \"turbo start\" script taking up cycles. In comparison, we stop turbo with\n; executesync(), because we want to block until the end of the frame, and\n; the end of the frame will naturally clean up the script.\nexecutesync(\"TE2.2:stop\")\nturbo.register += 1\n\n; Do another loop if we're not done yet.\n; Abort if the 3rd-to-last power stone is not the tier we expect it to be.\n; Because Buy runs after the combines, and won't have been stopped\n; in the case where we're not done yet, we can expect the \"fill inventory\" buy\n; to have left the final position with our target tier. Checking this way\n; catches both museum rollover and out-of-resources.\n;\n; The reason for checking the 3rd-to-last slot, and not the last slot, has to\n; do with the way combine processing is run: It starts checking for combinable\n; stuff from the first slot down. So it's possible for two base-tier stones to\n; be left in the last two slots, but there can't be *3* in the last 3, because\n; they would be combine-eligible. *Unless* they were just bought. (This isn't\n; quite true, because the combines might be occupied with higher-tier stones,\n; but if we've exausted resources or had rollover, it will rapidly become true.)\n;\n; This also interacts in predictable ways with another couple of common issues:\n; lacking the \"quick combine\" skill and not having all the inventory slots.\n; If all the slots aren't purchased, this condition will always fail immediately.\n; The symptom will be that the combiners seems to \"never do anything.\"\n; Conversely, without quick combine no progress will ever be made. As soon\n; as a power stone is hit that needs fast leveling, it will get \"stuck\" there.\n; This is a sure sign of lacking quick-combine.\n;\n; If we are doing a \"careful\" combine without fill-inventory, it shouldn't take\n; more than 1000 cycles, so this condition won't be a problem.\ngotoif(restart_loop,\\\n  tier(\"inventory\", 0) < target_tier && tier(\"inventory\", 27) == museum_tier)\n\ndone:\n; This is the signal for Main that it should continue.\nmuseum_tier = -2\n\n; Now that we're done, move the finished stone back to \"equipped\". This also\n; moves partial/original stones back, if we cancelled via {start}.\n;\n; Even though this happens after signalling, we get one extra cycle as a\n; \"delay slot\" before we're stopped by Main.\nmuseum.moveTo(\"inventory\", 0, \"loadout\", museum_pos)\n\ncombine:\n; Because of turbo exec, this script will keep running every cycle, until\n; the frame pause. At that point, it will exit, and we rely on Combine to\n; restart it.\n\n; There's no point in using a variable for the combine limit; halting the\n; combines is handled by the end of turbo. Having the limit be a constant\n; removes one node from the hottest execution path.\ncombine(50)\n"],["Calculate",":import museum_macros\n:name {script(Calculate)}\n\n; This script contains (stub) logic for handling key.{start}. This would be part\n; of Main normally, but there can only be 2 impulses per script at the\n; resource-cost-levels we're targeting.\n;\n; The final (bulk) of the logic deals with buying test stones from the\n; offshore market and doing calculations/preparations for the actual\n; combining, which is handled by Combine. This script sets museum_tier\n; and target_tier, which directly drive the combining process.\n\nkey.{start}()\n\n:global int museum_tier\n:global int target_tier\n:global int museum_pos\n:global double budget\n:global string offer_tiers\n\n:global int turbo.register\n:global int turbo.cycles\n:global int turbo.cycles.max\n\n:local int offer_idx\n\n; If we're called from key.{start}, then check if the museum\n; is open. If it is, signal Main, otherwise exit. We can't just set a\n; condition on the script, because that would possibly mess up scripts\n; that execute us, on exiting the museum.\n; This will overwrite target_tier if we are currently combining,\n; causing an early exit and leading to main resuming - which is what we\n; want.\n; If we're called from Main, then we need to set museum_tier to 0, because it\n; serves as a signal variable. Main waits on it for BuyOffshore/Combine to be done.\n;\n; It's ugly to fold this up into a single conditional set, but between the fact that\n; we need museum_tier reset on the first instruction, and some of the other conditions\n; involved, it wouldn't be less complicated to do it as two instructions.\nglobal.int.set(\\\n  if(contains(\"key.{start}\", impulse()), \"target_tier\", \"museum_tier\"),\\\n  if(contains(\"key.{start}\", impulse()),\\\n    if(isopen(\"museum\"), -2, target_tier),\\\n    0\\\n  )\\\n)\n; If we're called from key.{start}, we're done now. Otherwise, if offer_tiers is\n; the default, calculate it now, but don't bother re-doing work if we don't have to.\n; (This would be easier to do at the top, but there's no room in Main.)\ngoto(if(\\\n  contains(\"key.{start}\", impulse()),\\\n  end,\\\n  if(\\\n    contains(offer_tiers, \"0101010101010101-1\"),\\\n    get_offers,\\\n    skip_offers\\\n  )\\\n))\n\n; This loop calculates the best stone to buy, for each element.\nget_offers:\n\n; Turn an element string into an index. The base version is defined in museum_macros.\n#element_to_index(offset) {element_to_index_base(museum.slotElement(offer_idx), {offset})}\n\n; This monstrousity updates the value of the best tier to use for an element.\n; The element is the element in slot [offer_idx], and we're assessing whether\n; that offer will be an improvement.\n; The base logic relies on the top_tier formula.\noffer_tiers = if(\\\n  museum.slotElement(offer_idx) == \"\" ||\\\n    {top_tier(i2d(museum.slotTier(offer_idx)))} <=\\\n      {top_tier(s2d(sub(offer_tiers, {element_to_index(\"\")}, 2), -1.))},\\\n  offer_tiers,\\\n  sub(offer_tiers, 0, {element_to_index(\"\")}) .\\\n    sub(i2s(100 + museum.slotTier(offer_idx)), 1, 2) .\\\n    sub(offer_tiers, {element_to_index(\"  \")}, 99)\\\n)\n; There are max 10 offer slots, numbered 0 through 9. We'll exit after #9.\noffer_idx = offer_idx + 1\ngotoif(get_offers, offer_idx < 10)\n\nskip_offers:\nclear(\"inventory\")\n\n; Load the pre-calculated tier of stone we are buying.\nmuseum_tier = s2i(sub(\\\n  offer_tiers,\\\n  {element_to_index_base(element(\"loadout\", museum_pos), \"\")},\\\n  2\\\n), -1)\n\n; Determine \"target_tier\", the level we are trying to upgrade to. In the\n; best case, we can upgrade 11 levels past the tier of the stones we can\n; buy from the museum. (Which may only be tier 1.)\ntarget_tier = if(min(tier(\"loadout\", museum_pos), museum_tier) < 0,\\\n  -1,\\\n  d2i({top_tier(i2d(museum_tier))})\\\n)\n; If tier(museum_pos) < museum_tier, buy one of the target stone.\n; In this case, we're about to move our stone into the 0th slot, and\n; if it's below the base level it will never get combined with, so\n; the combine loop will never end.\n; It's OK to still move it, because there's enough room in the inventory\n; for an extra stone.\n; If we can't afford the stone, or for some other reason are unable to buy\n; it, then we'll also fall through in Combine and move the original stone\n; back when we're done. In this way, we can always avoid eating stones,\n; no matter what happens.\nmuseum.buyTier(\\\n  element(\"loadout\", museum_pos),\\\n  museum_tier,\\\n  if(tier(\"loadout\", museum_pos) < museum_tier, 1, 0)\\\n)\nmove(\"loadout\", museum_pos, \"inventory\")\n\n; Extend turbo for a little longer. There's a race condition where we might\n; run out of cycles and start a new frame, but even if we do that on this\n; line, the new frame will start in time for the Combiners to loop properly,\n; and for the max-setting line in Combine to set the max to its proper value.\n;\n; Otherwise, this ensures that we stay in the current loop until the line\n; in Combine sets cycles.max to its full value.\n; This is a small enough extension so that if we never enter the main loop\n; of Combine, we'll still eventually hit turbo.cycles.max and start a new frame.\n; (I.e. we can't loop indefinitely with this extension alone.)\nturbo.cycles.max = max(turbo.cycles.max, turbo.cycles + 15)\n\n; executesync() is used here to pause us until our child Combine stops us.\n; If we jump from the top, we have to be prepared to loop back to this instruction.\n; in that case, this needs to be a no-op.\nexecutesync(if(contains(\"key.{start}\", impulse()),\\\n  \"%%museum-nop%%\",\\\n  \"{script(Combine)}\"\\\n))\n\nend:\n"],["Main",":import museum_macros\n:name {script(Main)}\n\n; The \"main\" script of the combiner. It starts on wakeup/entry into the\n; museum, and keeps running until the user leaves the museum. This is because\n; it is responsible for maintaining the UI global \"museum_status\", which\n; both conveys information to the user and also ends a <size=0> block that\n; hides our internal global variables. When we leave the museum, this is\n; set to \"</size>\" to blank the display and leave no clutter.\n;\n; This script also runs the outer part of the loop, which updates the\n; position of the combiner. It is well-suited to this, because it is the\n; only script that doesn't get stop()'ed at some point.\n\n:global int max_craft_tier\n:global int museum_pos\n\n:global string museum_status\n:global string offer_tiers\n:global int museum_tier\n:global int target_tier\n\n:global int turbo.cycles\n:global int turbo.register\n\nwakeup()\nopen.museum()\n\nisopen(\"museum\")\n\ntop:\n; Launch Buy to initialize the global variables in the proper order.\n; We re-do this after every run, because it resets museum_pos for us.\nexecutesync(\"{script(Buy)}\")\n\n; Two characters per tier. Universal is last. These are initialized to what can\n; be bought from the store.\n; This logically belongs in \"Buy\", but is moved out of there to make space.\noffer_tiers = \"0101010101010101-1\"\n\n; Macro-substitution for the museum timer, allows mocking it out easily for\n; testing.\n#timer museum.timer()\n\n; No-offshore-market fix: If we get here with museum_tier == -2 (which is the\n; waiting-to-start state), but the timer is *exactly* 1 hour, this means the user\n; doesn't have the offshore market and is getting the default value. In this case,\n; reset museum tier so that the script stops properly.\nmuseum_tier = if(\\\n  museum_tier == -2 && {timer} == 60. * 60.,\\\n  0,\\\n  museum_tier\\\n)\n\n; This loop usually runs without turbo, although it can have lingering\n; turbo from other scripts without bad effects. It keeps the status line\n; up-to-date while the museum is not running.\nstatus_loop:\n; We use target_tier to receive signals from other scripts. 0 means nothing\n; is hapenning, so we always reset to 0 at the top.\ntarget_tier = 0\n\n; This is a useful sub-expression when displaying numbers in rounded\n; scientific notation. We want to extract the exponent, but for numbers like\n; .9996, we know they'll round up to 1.00 (when rounded to 3 places), so we\n; have to consider them as an exponent higher already.\n; The parameter is to allow for the injection of a constant for constant\n; folding in later expressions.\n#adjusted_exp(x) floor(gdg({budget}) // 10. - (0.9995 // 10. + {x}))\n\n; Stringify the budget in rounded-scientific notation, rounded to 3 digits\n; (2 after the decimal place). This is an awkwardly large expression, but\n; it's really the best we can do with the tools we have.\n; The \"2\" passed to adjusted_exp subtracts 2 from the exponent, so the\n; overall effect is to multiply by 100 (before rounding).\n#rounded_budget round(gdg({budget}) / (10. ^ {adjusted_exp(2.)})) / 100. .\\\n  \"e\". {adjusted_exp(0.)}\n\n; Normally we would take the ceiling of the time remaining, because that's\n; how timers work. (You show 1 second left until the time hits 0.)\n; However, the display timer in the museum uses floor, and we want to match\n; that, so we use floor too.\n; The parameter is a divisor, to make dealing with minutes easier.\n#time_floor(x) floor({timer}/ {x})\n\n; Set the status. There's a lot of cases to this:\n; * Error for no workers available.\n;   - We don't have direct visibilty to worker_slot, but Buy will signal the\n;     error by setting budget to negative, which will never happen otherwise.\n; * Error for a bad Turbo install. This is disturbingly frequent.\n; * Show our current budget, with green highlighting to prompt that this\n;   can be adjusted.\n; * Show a brief help line, also with green highlighting to link the keys\n;   to the budget.\n; * If we've pressed {start}, show the Combining message instead. This is\n;   important because certain things key off of it. target_tier = -2 is the\n;   signal for this.\n;\n; We don't bother doing anything special when the museum isn't open, since\n; we'll handle that at the bottom of the script.\nmuseum_status = if(\\\n  gdg({budget}) < 0.,\\\n  \"</size>error=<color=#fb3>No available workers!</color>\",\\\n  if(\\\n    turbo.cycles == 0,\\\n    \"</size>error=<color=#fb3>Turbo exec is not working</color>\",\\\n    \"</size>museum=<color=#2f4>\" .\\\n    {rounded_budget} .\\\n    \"</color> <color=#fff>budget</color><br>\\\n<color=#0df><color=#2f4>{up}</color>/<color=#2f4>{down}</color> changes, <color=#2f4>{start}</color> \" .\\\n    if(\\\n      museum_tier != -2,\\\n      \"begins</color>\",\\\n      \"stops</color><br><color=#fff>Waiting \" .\\\n      {time_floor(60.)} . \":\" . sub(d2s({time_floor(1.)} % 60. + 100.), 1, 2) .\\\n      \"</color>\"\\\n    )\\\n  )\\\n)\n; The s2i()/sub() expression is a jump-table, where the string values are\n; line numbers.\n; The values that target_tier can have when we get here are 0\n; (if it hasn't been set to anything since it was cleared at the top of\n; status_loop), -2 (set when key.{start} is pressed), and -3 (set when\n; budget is adjusted via key.{up}/key.{down}).\n;\n; -2 finishes the loop. -3 should reset target_tier and update status by\n; jumping two lines back. 0 *could* repeat the same line, except sometimes\n; we have to update status, so we jump one back, to set status.\n; We can't merge the -3 and 0 cases, because setting target_tier over-frequently\n; makes keystrokes flaky.\n;\n; We use a modified jump table when museum_tier is -2, which indicates that\n; we are synced to the refresh timer. In that case, we abort when the timer is\n; 59:59 (meaning the refresh just hapenned), and when start is pressed (-2 for\n; target_tier) we jump back to executing Buy, which will reset museum_tier\n; (and also do other things, which we don't care about) in that case.\ngoto(if(\\\n  isopen(\"museum\"),\\\n  if(\\\n    museum_tier == -2 && {timer} >= 59. * 60. + 59.,\\\n    start_museum,\\\n    s2i(sub(if(museum_tier == -2, \"41 5\", \"47 5\"), target_tier + 3, 1), 99)\\\n  ),\\\n  end\\\n))\n\nstart_museum:\n; Now that we're in the active part of the script, start turbo. We want\n; minimal overhead, so don't execute an extra script, just increment\n; the variable.\nturbo.register += 1\n\nupgrade_loop:\n; Go to the next script to perform the actual upgrade.\n; We run this even when museum_pos is -1, in order to set all the variables\n; properly. Combine will exit immediately in that case, without a frame break,\n; so we will fall down below and set the status correctly within the frame\n; that it starts.\nexecute(\"{script(Calculate)}\")\n\n; Combine (which gets run from Calculate) will signal us when it's done.\n; It must be stopped for proper cleanup.\nwaituntil(museum_tier == -2)\nstop(\"{script(Combine)}\")\n\nskip:\nmuseum_pos += 1\n; While running, we have fewer conditions to check, since the errors were\n; already signaled at the top. (There's nothing actually stopping the user\n; from starting the script anyway, but that's on them at that point.)\n;\n; The condition for displaying Combining is reversed, since it's usual\n; here, and if {start} is pressed it means we should exit.\nmuseum_status = \"</size>museum=<color=#2f4>\" .\\\n  {rounded_budget} .\\\n  \"</color> <color=#fff>budget</color><br>\" .\\\n  if(\\\n    target_tier != -2,\\\n    \"<color=#ff0>Combining... [\" . museum_pos . \"] <color=#2f4>{start}</color> stops</color>\",\\\n    \"<color=#0df><color=#2f4>{up}</color>/<color=#2f4>{down}</color> changes, <color=#2f4>{start}</color> begins</color>\"\\\n  )\n\nstop_turbo:\n; Most of the time, we do not want to stop turbo. We only want to do it in\n; the specific cases where we'll be ending the loop. So, this duplicates a lot\n; of the logic in the loop below, all for the benefit of saving a line.\n;\n; Note that if we jumped to stop_turbo directly, museum_status can never\n; contain \"Combining\".\nexecutesync(if(\\\n  isopen(\"museum\") && museum_pos < 130 && contains(museum_status, \"Combining\"),\\\n  \"%%museum-nop%%\",\\\n  \"TE2.2:stop\"\\\n))\n\n; This very complicated gotoif consolidates the ends of lots of loops into\n; one statement.\n; If the museum is closed, fall through to exit the script.\n; If we're through all the positions, or if we're no longer \"Combining\"\n; (which means {start} was pressed), go to the top to reset our state.\n;\n; Otherwise, continue the loop: Either normally, or via a shortcut if this\n; part of the grid is empty, to avoid executing the sub-scripts.\ngotoif(\\\n  if(\\\n    museum_pos < 130 && contains(museum_status, \"Combining\"),\\\n    if(tier(\"loadout\", museum_pos) == -1, skip, upgrade_loop),\\\n    top\\\n  ),\\\n  isopen(\"museum\")\\\n)\n\nend:\n; Before we exit, blank the status so that there isn't clutter on the screen.\n; This is safe to do in the last slot because turbo shouldn't be running by\n; this point. Even if it is, we're the only ones who set museum_status, so\n; it's still safe.\nmuseum_status = \"</size>\"\n"]]}}

You will need an additional library to compile, which is part of the factory package: worker_storage_lib.

If all you want to do is to change the keybindings, they are located in museum_macros, and you can hit "Export Workspace" to get a bundle code after changing them.

Changelog

v4.7

Increased the max cycles of the combiner to 50000, so that it is solely FPS limited, and won't run into a glitch that caused the script list to flicker and made it run slow.

Also exported in the smaller compressed format.

v4.6

Added an upper limit to the budget of 1e280, so it can't get stuck at "NaNeInfinity".

v4.5

Throttling is now time-based instead of turbo-cycles-based. All players should experience the same amount of jank, instead of it being much worse for people with slow/old computers. Also, the throttle was adjusted to 5 FPS, which should be enough to not appear super-janky.

v4.4

Fix an issue that occured without the Offshore Market: The script would immediately start a new upgrade cycle after the previous one was finished.

I'm unsure if this had always been hapenning, or if it was caused by a change in the game.

v4.3

Change "equipped" to "loadout" in the source. (No functionality change.) This is not strictly needed, because the old value still works, but it's good to stay current.

v4.2

Properly cap tiers at 50, to not waste time/resources when running at very high budgets.

v4.1

  • Bugfix that would cause eaten stones if the available tiers were too high for the budget, but there wasn't anything lower available.
  • Another bugfix that more comprehensively protects against all potential sources of eaten stones. Hopefully, they should be totally a thing of the past.
  • Add auto-determination of preferred tier.

v4.0

Very large rewrite of several core pieces, due to the game's museum overhaul.

  • Test stones are no longer used, instead the offers are directly inspected to determine the best tiers to buy. In some cases this leads to better outcomes.
  • Different functions are used, because the old ones for buying were deprecated.
  • Layouts with gaps are now handled fine.
  • The timer is now synced directly to the museum's timer, and so it work properly with power plant boosts.

v3.1

Fixed several bugs dealing with universal stones and low-level offers in the offshore market, particularly T1 universals. (Hopefully. I can't test some of these directly, because my museum is too upgraded.)

v3.0

Initial release. (Named v3 because the previous version of this name was v2, but the code was completely different.)