From 536845186754a9ae56e9d4a4fae97d661e272691 Mon Sep 17 00:00:00 2001
From: Mathx2
Date: Thu, 7 Sep 2023 13:23:42 -0700
Subject: [PATCH 001/144] Timespinner: Options.py Typo (#2154)
Line 63, changed the commend from (Reccomended) to (Recommended)
---
worlds/timespinner/Options.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py
index 5f4d230688..8b11184944 100644
--- a/worlds/timespinner/Options.py
+++ b/worlds/timespinner/Options.py
@@ -60,7 +60,7 @@ class BossRando(Toggle):
class BossScaling(DefaultOnToggle):
- "When Boss Rando is enabled, scales the bosses' HP, XP, and ATK to the stats of the location they replace (Reccomended)"
+ "When Boss Rando is enabled, scales the bosses' HP, XP, and ATK to the stats of the location they replace (Recommended)"
display_name = "Scale Random Boss Stats"
From 2b9e8fa273ceee19c59139e8744bb3a905c33e7b Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Sat, 9 Sep 2023 05:02:05 +0200
Subject: [PATCH 002/144] WebHost: flask caching doesn't do lazy init anymore
(#2155)
---
WebHost.py | 3 ++-
WebHostLib/__init__.py | 4 ++--
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/WebHost.py b/WebHost.py
index 45d017cf1f..36645ad27d 100644
--- a/WebHost.py
+++ b/WebHost.py
@@ -14,7 +14,7 @@ import settings
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
-from WebHostLib import register, app as raw_app
+from WebHostLib import register, cache, app as raw_app
from waitress import serve
from WebHostLib.models import db
@@ -40,6 +40,7 @@ def get_app():
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}")
+ cache.init_app(app)
db.bind(**app.config["PONY"])
db.generate_mapping(create_tables=True)
return app
diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py
index a59e3aa553..441f3272fd 100644
--- a/WebHostLib/__init__.py
+++ b/WebHostLib/__init__.py
@@ -49,11 +49,11 @@ app.config["PONY"] = {
'create_db': True
}
app.config["MAX_ROLL"] = 20
-app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache"
+app.config["CACHE_TYPE"] = "SimpleCache"
app.config["JSON_AS_ASCII"] = False
app.config["HOST_ADDRESS"] = ""
-cache = Cache(app)
+cache = Cache()
Compress(app)
From f6dafa2b560ff1f33070903ff0655d7340d9dfd2 Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Sat, 9 Sep 2023 05:02:53 +0200
Subject: [PATCH 003/144] Core: collect errors from generate_output at same
step as multidata
---
Main.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/Main.py b/Main.py
index fe56dc7d9e..860be6347c 100644
--- a/Main.py
+++ b/Main.py
@@ -392,7 +392,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
f.write(bytes([3])) # version of format
f.write(multidata)
- multidata_task = pool.submit(write_multidata)
+ output_file_futures.append(pool.submit(write_multidata))
if not check_accessibility_task.result():
if not world.can_beat_game():
raise Exception("Game appears as unbeatable. Aborting.")
@@ -400,7 +400,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.warning("Location Accessibility requirements not fulfilled.")
# retrieve exceptions via .result() if they occurred.
- multidata_task.result()
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
if i % 10 == 0 or i == len(output_file_futures):
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
From 29f8053d6ed256f87963ce17815dec9ea2484d85 Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Sun, 10 Sep 2023 00:33:36 +0200
Subject: [PATCH 004/144] Factorio: fix website multitracker (#2126)
Co-authored-by: Remy Jette
---
.../templates/multiFactorioTracker.html | 16 +++---
WebHostLib/tracker.py | 50 +++++++++++++------
2 files changed, 43 insertions(+), 23 deletions(-)
diff --git a/WebHostLib/templates/multiFactorioTracker.html b/WebHostLib/templates/multiFactorioTracker.html
index e8fa7b152c..faca756ee9 100644
--- a/WebHostLib/templates/multiFactorioTracker.html
+++ b/WebHostLib/templates/multiFactorioTracker.html
@@ -27,14 +27,14 @@
{% endblock %}
{% block custom_table_row scoped %}
{% if games[player] == "Factorio" %}
-{% set player_inventory = inventory[team][player] %}
-{% set prog_science = player_inventory[custom_items["progressive-science-pack"]] %}
-
{% if player_inventory[custom_items["logistic-science-pack"]] or prog_science %}✔{% endif %}
-
{% if player_inventory[custom_items["military-science-pack"]] or prog_science > 1%}✔{% endif %}
-
{% if player_inventory[custom_items["chemical-science-pack"]] or prog_science > 2%}✔{% endif %}
-
{% if player_inventory[custom_items["production-science-pack"]] or prog_science > 3%}✔{% endif %}
-
{% if player_inventory[custom_items["utility-science-pack"]] or prog_science > 4%}✔{% endif %}
-
{% if player_inventory[custom_items["space-science-pack"]] or prog_science > 5%}✔{% endif %}
+{% set player_inventory = named_inventory[team][player] %}
+{% set prog_science = player_inventory["progressive-science-pack"] %}
+
{% if player_inventory["logistic-science-pack"] or prog_science %}✔{% endif %}
+
{% if player_inventory["military-science-pack"] or prog_science > 1%}✔{% endif %}
+
{% if player_inventory["chemical-science-pack"] or prog_science > 2%}✔{% endif %}
+
{% if player_inventory["production-science-pack"] or prog_science > 3%}✔{% endif %}
+
{% if player_inventory["utility-science-pack"] or prog_science > 4%}✔{% endif %}
+
{% if player_inventory["space-science-pack"] or prog_science > 5%}✔{% endif %}
{% else %}
❌
❌
diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py
index 80ccc720a3..5b89495ecc 100644
--- a/WebHostLib/tracker.py
+++ b/WebHostLib/tracker.py
@@ -11,7 +11,7 @@ from werkzeug.exceptions import abort
from MultiServer import Context, get_saving_second
from NetUtils import SlotType, NetworkSlot
from Utils import restricted_loads
-from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package
+from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package, games
from worlds.alttp import Items
from . import app, cache
from .models import GameDataPackage, Room
@@ -1423,9 +1423,12 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s
)
-def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]:
- inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in team_data}
- for teamnumber, team_data in data["checks_done"].items()}
+def _get_inventory_data(data: typing.Dict[str, typing.Any]) \
+ -> typing.Dict[int, typing.Dict[int, typing.Dict[int, int]]]:
+ inventory: typing.Dict[int, typing.Dict[int, typing.Dict[int, int]]] = {
+ teamnumber: {playernumber: collections.Counter() for playernumber in team_data}
+ for teamnumber, team_data in data["checks_done"].items()
+ }
groups = data["groups"]
@@ -1444,6 +1447,17 @@ def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int,
return inventory
+def _get_named_inventory(inventory: typing.Dict[int, int], custom_items: typing.Dict[int, str] = None) \
+ -> typing.Dict[str, int]:
+ """slow"""
+ if custom_items:
+ mapping = collections.ChainMap(custom_items, lookup_any_item_id_to_name)
+ else:
+ mapping = lookup_any_item_id_to_name
+
+ return collections.Counter({mapping.get(item_id, None): count for item_id, count in inventory.items()})
+
+
@app.route('/tracker/')
@cache.memoize(timeout=60) # multisave is currently created at most every minute
def get_multiworld_tracker(tracker: UUID):
@@ -1455,18 +1469,22 @@ def get_multiworld_tracker(tracker: UUID):
return render_template("multiTracker.html", **data)
+if "Factorio" in games:
+ @app.route('/tracker//Factorio')
+ @cache.memoize(timeout=60) # multisave is currently created at most every minute
+ def get_Factorio_multiworld_tracker(tracker: UUID):
+ data = _get_multiworld_tracker_data(tracker)
+ if not data:
+ abort(404)
-@app.route('/tracker//Factorio')
-@cache.memoize(timeout=60) # multisave is currently created at most every minute
-def get_Factorio_multiworld_tracker(tracker: UUID):
- data = _get_multiworld_tracker_data(tracker)
- if not data:
- abort(404)
+ data["inventory"] = _get_inventory_data(data)
+ data["named_inventory"] = {team_id : {
+ player_id: _get_named_inventory(inventory, data["custom_items"])
+ for player_id, inventory in team_inventory.items()
+ } for team_id, team_inventory in data["inventory"].items()}
+ data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio")
- data["inventory"] = _get_inventory_data(data)
- data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio")
-
- return render_template("multiFactorioTracker.html", **data)
+ return render_template("multiFactorioTracker.html", **data)
@app.route('/tracker//A Link to the Past')
@@ -1596,5 +1614,7 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = {
multi_trackers: typing.Dict[str, typing.Callable] = {
"A Link to the Past": get_LttP_multiworld_tracker,
- "Factorio": get_Factorio_multiworld_tracker,
}
+
+if "Factorio" in games:
+ multi_trackers["Factorio"] = get_Factorio_multiworld_tracker
From a1418ccb66c5e8814e61ddb41469dd5ea307d4ea Mon Sep 17 00:00:00 2001
From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
Date: Sat, 9 Sep 2023 21:30:03 -0400
Subject: [PATCH 005/144] Docs: Small typo and proofreading edits (#2078)
* Slight rewording of DS3 game page
Lists made more concise, space added between "generated weapons" and open parenthesis
* Proofread Final Fantasy pages
Fixed minor typos and reworded sentences for conciseness.
* Edited Kingdom Hearts 2 Game Page
Refined style, capitalization, and sentence structure for clarity
* Fixed nested list in Minecraft game page
Each nest needed an additional 2 spaces
* Edited Risk of Rain 2 Game Page
Made various edits to redundancy within the page as well as omitted/unclear information
* Edited Stardew Valley game page
Small capitalization consistency edits and slight rewording for conciseness
* Update worlds/ff1/docs/multiworld_en.md
Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
* Update worlds/kh2/docs/en_Kingdom Hearts 2.md
Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
* Update worlds/kh2/docs/en_Kingdom Hearts 2.md
Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
* Add information for EXP multiplier
Include Drive Forms and Summons
* Correction for Newt Altars RoR2
Co-Authored-By: kindasneaki <19377912+kindasneaki@users.noreply.github.com>
---------
Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
Co-authored-by: kindasneaki <19377912+kindasneaki@users.noreply.github.com>
---
worlds/dark_souls_3/docs/en_Dark Souls III.md | 15 +-
worlds/ff1/docs/en_Final Fantasy.md | 18 +--
worlds/ff1/docs/multiworld_en.md | 8 +-
worlds/kh2/docs/en_Kingdom Hearts 2.md | 29 ++--
worlds/minecraft/docs/en_Minecraft.md | 128 +++++++++---------
worlds/ror2/docs/en_Risk of Rain 2.md | 55 ++++----
.../stardew_valley/docs/en_Stardew Valley.md | 14 +-
7 files changed, 138 insertions(+), 129 deletions(-)
diff --git a/worlds/dark_souls_3/docs/en_Dark Souls III.md b/worlds/dark_souls_3/docs/en_Dark Souls III.md
index 3ad8236ccf..1b55593bad 100644
--- a/worlds/dark_souls_3/docs/en_Dark Souls III.md
+++ b/worlds/dark_souls_3/docs/en_Dark Souls III.md
@@ -7,19 +7,18 @@ config file.
## What does randomization do to this game?
-In Dark Souls III, all unique items you can earn from a static corpse, a chest or the death of a Boss/NPC are
+In Dark Souls III, all unique items you can earn from a static corpse, chest, or the death of a Boss/NPC are
randomized.
-An option is available from the settings page to also randomize the upgrade materials, the Estus shards and the
-consumables.
-Another option is available to randomize the level of the generated weapons(from +0 to +10/+5)
+An option is available from the settings page to also randomize upgrade materials, Estus shards, and consumables.
+Another option is available to randomize the level of the generated weapons (from +0 to +10/+5).
-To beat the game you need to collect the 4 "Cinders of a Lord" randomized in the multiworld
-and kill the final boss "Soul of Cinder"
+To beat the game, you need to collect the 4 "Cinders of a Lord" randomized in the multiworld
+and kill the final boss "Soul of Cinder."
## What Dark Souls III items can appear in other players' worlds?
-Every unique item from Dark Souls III can appear in other player's worlds, such as a piece of armor, an upgraded weapon,
-or a key item.
+Every unique item from Dark Souls III can appear in other player's worlds, such as a piece of armor, upgraded weapons,
+or key items.
## What does another world's item look like in Dark Souls III?
diff --git a/worlds/ff1/docs/en_Final Fantasy.md b/worlds/ff1/docs/en_Final Fantasy.md
index a62b5ec126..29d4d29f80 100644
--- a/worlds/ff1/docs/en_Final Fantasy.md
+++ b/worlds/ff1/docs/en_Final Fantasy.md
@@ -8,19 +8,19 @@ website: [FF1R Website](https://finalfantasyrandomizer.com/)
## What does randomization do to this game?
-A better questions is what isn't randomized at this point. Enemies stats and spell, character spells, shop inventory and
-boss stats and spells are all commonly randomized. Unlike most other randomizers it is also most standard to shuffle
-progression items and non-progression items into separate pools and then redistribute them to their respective
-locations. So, for example, Princess Sarah may have the CANOE instead of the LUTE; however, she will never have a Heal
-Pot or some armor. There are plenty of other things that can be randomized on the main randomizer
-site: [FF1R Website](https://finalfantasyrandomizer.com/)
+Enemy stats and spell, boss stats and spells, character spells, and shop inventories are all commonly randomized. Unlike
+most other randomizers, it is standard to shuffle progression items and non-progression items into separate pools
+and then redistribute them to their respective locations. For example, Princess Sarah may have the CANOE instead
+of the LUTE; however, she will never have a Heal Pot or armor.
+
+Plenty of other things to be randomized can be found on the main randomizer site:
+[FF1R Website](https://finalfantasyrandomizer.com/)
## What Final Fantasy items can appear in other players' worlds?
-All items can appear in other players worlds. This includes consumables, shards, weapons, armor and, of course, key
-items.
+All items can appear in other players worlds, including consumables, shards, weapons, armor, and key items.
## What does another world's item look like in Final Fantasy
-All local and remote items appear the same. It will say that you received an item and then BOTH the client log and the
+All local and remote items appear the same. Final Fantasy will say that you received an item, then BOTH the client log and the
emulator will display what was found external to the in-game text box.
diff --git a/worlds/ff1/docs/multiworld_en.md b/worlds/ff1/docs/multiworld_en.md
index 51fcd9b7bf..d3dc457f01 100644
--- a/worlds/ff1/docs/multiworld_en.md
+++ b/worlds/ff1/docs/multiworld_en.md
@@ -32,14 +32,14 @@ Generate a game by going to the site and performing the following steps:
prefer, or it is your first time we suggest starting with the 'Shard Hunt' preset (which requires you to collect a
number of shards to go to the end dungeon) or the 'Beginner' preset if you prefer to kill the original fiends.
2. Go to the `Goal` tab and ensure `Archipelago` is enabled. Set your player name to any name that represents you.
-3. Upload you `Final Fantasy(USA).nes` (and click `Remember ROM` for the future!)
+3. Upload your `Final Fantasy(USA).nes` (and click `Remember ROM` for the future!)
4. Press the `NEW` button beside `Seed` a few times
5. Click `GENERATE ROM`
-It should download two files. One is the `*.nes` file which your emulator will run and the other is the yaml file
+It should download two files. One is the `*.nes` file which your emulator will run, and the other is the yaml file
required by Archipelago.gg
-At this point you are ready to join the multiworld. If you are uncertain on how to generate, host or join a multiworld
+At this point, you are ready to join the multiworld. If you are uncertain on how to generate, host, or join a multiworld,
please refer to the [game agnostic setup guide](/tutorial/Archipelago/setup/en).
## Running the Client Program and Connecting to the Server
@@ -67,7 +67,7 @@ Once the Archipelago server has been hosted:
## Play the game
-When the client shows both NES and server are connected you are good to go. You can check the connection status of the
+When the client shows both NES and server are connected, you are good to go. You can check the connection status of the
NES at any time by running `/nes`
### Other Client Commands
diff --git a/worlds/kh2/docs/en_Kingdom Hearts 2.md b/worlds/kh2/docs/en_Kingdom Hearts 2.md
index d132b29ca4..8258a099cc 100644
--- a/worlds/kh2/docs/en_Kingdom Hearts 2.md
+++ b/worlds/kh2/docs/en_Kingdom Hearts 2.md
@@ -2,7 +2,7 @@
Changes from the vanilla game
-This randomizer takes Kingdom Hearts 2 and randomizes the locations of the items for a more dynamic play experience. The items that randomize currently are all items within Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels. This allows abilities that Sora would normally have to also be placed on Keyblades with random stats. With several options on ways to finish the game.
+This randomizer creates a more dynamic play experience by randomizing the locations of most items in Kingdom Hearts 2. Currently all items within Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels are randomized. This allows abilities that Sora would normally have to be placed on Keyblades with random stats. Additionally, there are several options for ways to finish the game, allowing for different goals beyond beating the final boss.
Where is the settings page
@@ -12,12 +12,18 @@ The [player settings page for this game](../player-settings) contains all the op
What is randomized in this game?
-The Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels.
+- Chests
+- Popups
+- Get Bonuses
+- Form Levels
+- Sora's Levels
+- Keyblade Stats
+- Keyblade Abilities
What Kingdom Hearts 2 items can appear in other players' worlds?
-Every item in the game with the exception being party members' abilities.
+Every item in the game except for party members' abilities.
What is The Garden of Assemblage "GoA"?
@@ -37,10 +43,10 @@ It is added to your inventory. If you obtain magic, you will need to pause your
What Happens if I die before Room Saving?
-When you die in Kingdom Hearts 2, you are reverted to the last non-boss room you entered and your status is reverted to what it was at that time. However, in archipelago, any item that you have sent/received will not be taken away from the player, any chest you have opened will remain open, and you will keep your level but lose the expereince. Unlike vanilla Kingdom Hearts 2.
+When you die in vanilla Kingdom Hearts 2, you are reverted to the last non-boss room you entered and your status is reverted to what it was at that time. However, in archipelago, any item that you have sent/received will not be taken away from the player, any chest you have opened will remain open, and you will keep your level, but lose the experience.
-For example, if you are fighting Roxas and you receive Reflect Element and you die fighting Roxas, you will keep that reflect. You will still need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.
+For example, if you are fighting Roxas, receive Reflect Element, then die mid-fight, you will keep that Reflect Element. You will still need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.
Customization options:
@@ -49,13 +55,12 @@ For example, if you are fighting Roxas and you receive Reflect Element and you d
1. Obtain Three Proofs.
2. Obtain a desired amount of Lucky Emblems.
3. Obtain a desired amount of Bounties that are on late locations.
-- Customize how many World Locking Items You Need to Progress in that World.
-- Customize the Amount of World Locking Items You Start With.
-- Customize how many locations you want on Sora's Levels.
-- Customize the EXP Multiplier of everything that affects Sora.
-- Customize the Available Abilities on Keyblades.
-- Customize the level of Progressive Movement (Growth Abilities) you start with.
-- Customize the amount of Progressive Movement (Growth Abilities) you start with.
+- Customize how many World-Locking Items you need to progress in that world.
+- Customize the amount of World-Locking Items you start with.
+- Customize how many of Sora's Levels are locations.
+- Customize the EXP multiplier for Sora, his Drive Forms, and his Summons.
+- Customize the available abilities on keyblades.
+- Customize the amount and level of progressive movement (Growth Abilities) you start with.
- Customize start inventory, i.e., begin every run with certain items or spells of your choice.
Quality of life:
diff --git a/worlds/minecraft/docs/en_Minecraft.md b/worlds/minecraft/docs/en_Minecraft.md
index 2d4f063b79..1ef347983b 100644
--- a/worlds/minecraft/docs/en_Minecraft.md
+++ b/worlds/minecraft/docs/en_Minecraft.md
@@ -29,82 +29,82 @@ sequence either by skipping it or watching hit play out.
## Which recipes are locked?
* Archery
- * Bow
- * Arrow
- * Crossbow
+ * Bow
+ * Arrow
+ * Crossbow
* Brewing
- * Blaze Powder
- * Brewing Stand
+ * Blaze Powder
+ * Brewing Stand
* Enchanting
- * Enchanting Table
- * Bookshelf
+ * Enchanting Table
+ * Bookshelf
* Bucket
* Flint & Steel
* All Beds
* Bottles
* Shield
* Fishing Rod
- * Fishing Rod
- * Carrot on a Stick
- * Warped Fungus on a Stick
+ * Fishing Rod
+ * Carrot on a Stick
+ * Warped Fungus on a Stick
* Campfire
- * Campfire
- * Soul Campfire
+ * Campfire
+ * Soul Campfire
* Spyglass
* Lead
* Progressive Weapons
- * Tier I
- * Stone Sword
- * Stone Axe
- * Tier II
- * Iron Sword
- * Iron Axe
- * Tier III
- * Diamond Sword
- * Diamond Axe
+ * Tier I
+ * Stone Sword
+ * Stone Axe
+ * Tier II
+ * Iron Sword
+ * Iron Axe
+ * Tier III
+ * Diamond Sword
+ * Diamond Axe
* Progessive Tools
- * Tier I
- * Stone Shovel
- * Stone Hoe
- * Tier II
- * Iron Shovel
- * Iron Hoe
- * Tier III
- * Diamond Shovel
- * Diamond Hoe
- * Netherite Ingot
+ * Tier I
+ * Stone Shovel
+ * Stone Hoe
+ * Tier II
+ * Iron Shovel
+ * Iron Hoe
+ * Tier III
+ * Diamond Shovel
+ * Diamond Hoe
+ * Netherite Ingot
* Progressive Armor
- * Tier I
- * Iron Helmet
- * Iron Chestplate
- * Iron Leggings
- * Iron Boots
- * Tier II
- * Diamond Helmet
- * Diamond Chestplate
- * Diamond Leggings
- * Diamond Boots
+ * Tier I
+ * Iron Helmet
+ * Iron Chestplate
+ * Iron Leggings
+ * Iron Boots
+ * Tier II
+ * Diamond Helmet
+ * Diamond Chestplate
+ * Diamond Leggings
+ * Diamond Boots
* Progressive Resource Crafting
- * Tier I
- * Iron Ingot from Nuggets
- * Iron Nugget
- * Gold Ingot from Nuggets
- * Gold Nugget
- * Furnace
- * Blast Furnace
- * Tier II
- * Redstone
- * Redstone Block
- * Glowstone
- * Iron Ingot from Iron Block
- * Iron Block
- * Gold Ingot from Gold Block
- * Gold Block
- * Diamond
- * Diamond Block
- * Netherite Block
- * Netherite Ingot from Netherite Block
- * Anvil
- * Emerald
- * Emerald Block
- * Copper Block
+ * Tier I
+ * Iron Ingot from Nuggets
+ * Iron Nugget
+ * Gold Ingot from Nuggets
+ * Gold Nugget
+ * Furnace
+ * Blast Furnace
+ * Tier II
+ * Redstone
+ * Redstone Block
+ * Glowstone
+ * Iron Ingot from Iron Block
+ * Iron Block
+ * Gold Ingot from Gold Block
+ * Gold Block
+ * Diamond
+ * Diamond Block
+ * Netherite Block
+ * Netherite Ingot from Netherite Block
+ * Anvil
+ * Emerald
+ * Emerald Block
+ * Copper Block
diff --git a/worlds/ror2/docs/en_Risk of Rain 2.md b/worlds/ror2/docs/en_Risk of Rain 2.md
index ca22d1a44d..d30edf8889 100644
--- a/worlds/ror2/docs/en_Risk of Rain 2.md
+++ b/worlds/ror2/docs/en_Risk of Rain 2.md
@@ -8,7 +8,7 @@ config file.
## What does randomization do to this game?
Risk of Rain is already a random game, by virtue of being a roguelite. The Archipelago mod implements pure multiworld
-functionality in which certain chests (made clear via a location check progress bar) will send an item out to the
+functionality in which certain chests will send an item out to the
multiworld. The items that _would have been_ in those chests will be returned to the Risk of Rain player via grants by
other players in other worlds.
@@ -16,28 +16,30 @@ There are two modes in risk of rain. Classic Mode and Explore Mode
Classic Mode:
- - Classic mode implements pure multiworld
-functionality in which certain chests (made clear via a location check progress bar) will send an item out to the
-multiworld. The items that _would have been_ in those chests will be returned to the Risk of Rain player via grants by
-other players in other worlds.
+ - Certain chests (made clear via a location check progress bar) will send an item out to the
+ multiworld. The location of these chests do not matter, since all environments share a unified location pool.
Explore Mode:
- - Just like in Classic mode chests will send out an item to the multiworld. The difference is that each environment
- will have a set amount that can be sent out and shrines along with other things that will need to be checked.
- Also, each environment is an item and, you'll need it to be able to access it.
+ - Chests will continue to work as they did in Classic Mode, the difference being that each environment
+ will have a set amount of items that can be sent out. In addition, shrines, radio scanners, newt altars,
+ and scavenger bags will need to be checked, depending on your settings.
+ This mode also makes each environment an item. In order to access a particular stage, you'll need it to be
+ sent in the multiworld.
## What is the goal of Risk of Rain 2 in Archipelago?
-Just like in the original game, any way to "beat the game" counts as a win. Alternatively, if you are new to the game and
+Just like in the original game, any way to "beat the game" counts as a win. This means beating one of the bosses
+on Commencement, The Planetarium, or A Moment, Whole. Alternatively, if you are new to the game and
aren't very confident in being able to "beat the game", you can set **Final Stage Death is Win** to true
-(You can turn this on in your player settings.) This will make it so if you die on either Commencement or The Planetarium,
-it will count as your goal, and **Obliterating yourself** will count as well.
+(You can turn this on in your player settings.) This will make it so dying on either Commencement or The Planetarium,
+or **obliterating yourself in A Moment, Fractured** will count as your goal.
**You do not need to complete all the location checks** to win; any item you don't collect may be released if the
server options allow.
If you die before you accomplish your goal, you can start a new run. You will start the run with any items that you
-received from other players. Any items that you picked up the "normal" way will be lost.
+received from other players. However, these items will be randomized within their rarity at the start of each run.
+Any items that you picked up the "normal" way will be lost.
Note, you can play Simulacrum mode as part of an Archipelago, but you can't achieve any of the victory conditions in
Simulacrum. So you could, for example, collect most of your items through a Simulacrum run(only works in classic mode),
@@ -72,10 +74,10 @@ The Risk of Rain items are:
Each item grants you a random in-game item from the category it belongs to.
-When an item is granted by another world to the Risk of Rain player (one of the items listed above) then a random
+When an item is granted by another world to the Risk of Rain player then a random
in-game item of that tier will appear in the Risk of Rain player's inventory. If the item grant is an `Equipment` and
-the player already has an equipment item equipped then the _item that was equipped_ will be dropped on the ground and _
-the new equipment_ will take it's place. (If you want the old one back, pick it up.)
+the player already has an equipment item equipped then the _item that was equipped_ will be dropped on the ground and
+_the new equipment_ will take it's place.
Explore Mode items are:
@@ -93,10 +95,10 @@ Dlc_Sotv items
* `Sulfur Pools`
* `Void Locus`
-When a explore item is granted it will unlock that environment and will now be accessible to progress to victory! The
-game will still pick randomly which environment is next but it will first check to see if they are available. If you have
-them unlocked it will weight the game to have a ***higher chance*** to go to one you have checks versus one you have
-already completed. You will still not be able to goto a stage 3 environment from a stage 1 environment.
+When an explore item is granted, it will unlock that environment and will now be accessible! The
+game will still pick randomly which environment is next, but it will first check to see if they are available. If you have
+multiple of the next environments unlocked, it will weight the game to have a ***higher chance*** to go to one you
+have checks in versus one you have already completed. You will still be unable to go to a stage 3 environment from a stage 1 environment.
@@ -108,9 +110,9 @@ to 250** items. The number of items will be randomized between all players, so y
item pickup step based on how many items the other players in the multiworld have. (Around 100 seems to be a good
ballpark if you want to have a similar number of items to most other games.)
-In explore mode the amount of checks base on how many **chests, shrines, scavengers, radio scanners and, newt altars**
-are in the pool. With just the base game the numbers are **52 to 516** and with the dlc its **60 to 660** with
-everything on default being **216**
+In explore mode, the amount of checks are based on how many **chests, shrines, scavengers, radio scanners, and newt altars**
+are in the pool. With just the base game, checks can range from **52 to 516**, with the DLC expanding it to **60 to 660**.
+Leaving everything on default, the total number of checks comes out to **216** locations.
After you have completed the specified number of checks, you won't send anything else to the multiworld. You can
receive up to the specified number of randomized items from the multiworld as the players find them. In either case,
@@ -120,12 +122,15 @@ you can continue to collect items as normal in Risk of Rain 2 if you've already
When the Risk of Rain player fills up their location check bar then the next spawned item will become an item grant for
another player's world (or possibly get sent back to yourself). The item in Risk of Rain will disappear in a poof of
-smoke and the grant will automatically go out to the multiworld.
+smoke and the grant will automatically go out to the multiworld. Additionally, you will see a message in the chat saying
+what item you sent out. If the message does not appear, this likely means that another game has collected their items from you.
## What is the item pickup step?
-The item pickup step is a YAML setting which allows you to set how many items you need to spawn before the _next_ item
-that is spawned disappears (in a poof of smoke) and goes out to the multiworld.
+The item pickup step is a setting in the YAML which allows you to set how many items you need to spawn before the _next_ item
+that is spawned disappears (in a poof of smoke) and goes out to the multiworld. For instance, an item step of **1** means that
+every other chest will send an item to the multiworld. An item step of **2** means that every third chest sends out an item
+just as an item step of **0** would send an item on **each chest.**
## Is Archipelago compatible with other Risk of Rain 2 mods?
diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md
index 042163343e..a880a40b97 100644
--- a/worlds/stardew_valley/docs/en_Stardew Valley.md
+++ b/worlds/stardew_valley/docs/en_Stardew Valley.md
@@ -7,7 +7,7 @@ config file.
## What does randomization do to this game?
-A vast number of optional objectives in stardew valley can be shuffled around the multiworld. Most of these are optional, and the player can customize their experience in their YAML file.
+A vast number of objectives in Stardew Valley can be shuffled around the multiworld. Most of these are optional, and the player can customize their experience in their YAML file.
For these objectives, if they have a vanilla reward, this reward will instead be an item in the multiworld. For the remaining number of such objectives, there are a number of "Resource Pack" items, which are simply an item or a stack of items that may be useful to the player.
@@ -28,24 +28,24 @@ The player can choose from a number of goals, using their YAML settings.
Location checks in Stardew Valley always include:
- [Community Center Bundles](https://stardewvalleywiki.com/Bundles)
-- [Mineshaft chest rewards](https://stardewvalleywiki.com/The_Mines#Remixed_Rewards)
+- [Mineshaft Chest Rewards](https://stardewvalleywiki.com/The_Mines#Remixed_Rewards)
- [Story Quests](https://stardewvalleywiki.com/Quests#List_of_Story_Quests)
-- [Traveling Merchant items](https://stardewvalleywiki.com/Traveling_Cart)
+- [Traveling Merchant Items](https://stardewvalleywiki.com/Traveling_Cart)
- Isolated objectives such as the [beach bridge](https://stardewvalleywiki.com/The_Beach#Tide_Pools), [Old Master Cannoli](https://stardewvalleywiki.com/Secret_Woods#Old_Master_Cannoli), [Grim Reaper Statue](https://stardewvalleywiki.com/Golden_Scythe), etc
There also are a number of location checks that are optional, and individual players choose to include them or not in their shuffling:
- Tools and Fishing Rod Upgrades
- Carpenter Buildings
- Backpack Upgrades
-- Mine elevator levels
+- Mine Elevator Levels
- Skill Levels
- Arcade Machines
-- Help Wanted quests
+- Help Wanted Quests
- Participating in Festivals
- Special Orders from the town board, or from Mr Qi
-- Cropsanity: Growing and harvesting individual crop types
+- Cropsanity: Growing and Harvesting individual crop types
- Fishsanity: Catching individual fish
-- Museumsanity: Donating individual items to the museum, or reaching the museum milestones for donations
+- Museumsanity: Donating individual items, or reaching milestones for museum donations
- Friendsanity: Reaching specific friendship levels with NPCs
## Which items can be in another player's world?
From faf4887616042eafa7e890a0e430b01a08e309c8 Mon Sep 17 00:00:00 2001
From: Brooty Johnson <83629348+Br00ty@users.noreply.github.com>
Date: Sat, 9 Sep 2023 21:33:57 -0400
Subject: [PATCH 006/144] DS3: add more options to slot_data for autotracking
(#2148)
---
worlds/dark_souls_3/__init__.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py
index faf6c28121..5d845e3ccc 100644
--- a/worlds/dark_souls_3/__init__.py
+++ b/worlds/dark_souls_3/__init__.py
@@ -502,6 +502,15 @@ class DarkSouls3World(World):
slot_data = {
"options": {
+ "enable_weapon_locations": self.multiworld.enable_weapon_locations[self.player].value,
+ "enable_shield_locations": self.multiworld.enable_shield_locations[self.player].value,
+ "enable_armor_locations": self.multiworld.enable_armor_locations[self.player].value,
+ "enable_ring_locations": self.multiworld.enable_ring_locations[self.player].value,
+ "enable_spell_locations": self.multiworld.enable_spell_locations[self.player].value,
+ "enable_key_locations": self.multiworld.enable_key_locations[self.player].value,
+ "enable_boss_locations": self.multiworld.enable_boss_locations[self.player].value,
+ "enable_npc_locations": self.multiworld.enable_npc_locations[self.player].value,
+ "enable_misc_locations": self.multiworld.enable_misc_locations[self.player].value,
"auto_equip": self.multiworld.auto_equip[self.player].value,
"lock_equip": self.multiworld.lock_equip[self.player].value,
"no_weapon_requirements": self.multiworld.no_weapon_requirements[self.player].value,
From bf685dc85045286b9984bec24401b70246286b09 Mon Sep 17 00:00:00 2001
From: Bicoloursnake <60069210+Bicoloursnake@users.noreply.github.com>
Date: Sat, 9 Sep 2023 21:51:12 -0400
Subject: [PATCH 007/144] Docs, SM64, SC2: Minor Documentation Updates (#2008)
* Update SC2 setup guide
Removed a sentence that made sense when I included sudo in the command in the previous sentence, but does not make sense otherwise.
* Update en_Super Mario 64.md
It turns out castle has a lowercase l in it.
---
worlds/sc2wol/docs/setup_en.md | 2 +-
worlds/sm64ex/docs/en_Super Mario 64.md | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/worlds/sc2wol/docs/setup_en.md b/worlds/sc2wol/docs/setup_en.md
index 13c7cb91e3..419f98a733 100644
--- a/worlds/sc2wol/docs/setup_en.md
+++ b/worlds/sc2wol/docs/setup_en.md
@@ -49,7 +49,7 @@ specific description of what's going wrong and attach your log file to your mess
## Running in macOS
-To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](https://archipelago.gg/tutorial/Archipelago/mac/en). Note: when running the client, you will need to run the command `python3 Starcraft2Client.py`. This is done to make sure that `/download_data` works correctly.
+To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](https://archipelago.gg/tutorial/Archipelago/mac/en). Note: when running the client, you will need to run the command `python3 Starcraft2Client.py`.
## Running in Linux
diff --git a/worlds/sm64ex/docs/en_Super Mario 64.md b/worlds/sm64ex/docs/en_Super Mario 64.md
index 4586369e5e..def6e2a375 100644
--- a/worlds/sm64ex/docs/en_Super Mario 64.md
+++ b/worlds/sm64ex/docs/en_Super Mario 64.md
@@ -14,7 +14,7 @@ as different Items from within SM64.
As in most Mario Games, save the Princess!
## Which items can be in another player's world?
-Any of the 120 Stars, and the two Caste Keys. Additionally, Cap Switches are also considered "Items" and the "!"-Boxes will only be active
+Any of the 120 Stars, and the two Castle Keys. Additionally, Cap Switches are also considered "Items" and the "!"-Boxes will only be active
when someone collects the corresponding Cap Switch Item.
## What does another world's item look like in SM64EX?
@@ -25,4 +25,4 @@ and who will receive it.
When you receive an Item, a Message will pop up to inform you where you received the Item from,
and which one it is.
-NOTE: The Secret Star count in the Menu is broken.
\ No newline at end of file
+NOTE: The Secret Star count in the Menu is broken.
From 2bdb1b2029c76258b9101ecede4f585f22542888 Mon Sep 17 00:00:00 2001
From: Bryce Wilson
Date: Sat, 9 Sep 2023 19:41:52 -0700
Subject: [PATCH 008/144] DS3: Update game page (#2163)
* DS3: Update game page
* DS3: Split long sentence in game page docs
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
* DS3: Minor word change
---------
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
---
worlds/dark_souls_3/docs/en_Dark Souls III.md | 21 +++++++++++--------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/worlds/dark_souls_3/docs/en_Dark Souls III.md b/worlds/dark_souls_3/docs/en_Dark Souls III.md
index 1b55593bad..e844925df1 100644
--- a/worlds/dark_souls_3/docs/en_Dark Souls III.md
+++ b/worlds/dark_souls_3/docs/en_Dark Souls III.md
@@ -7,19 +7,22 @@ config file.
## What does randomization do to this game?
-In Dark Souls III, all unique items you can earn from a static corpse, chest, or the death of a Boss/NPC are
-randomized.
-An option is available from the settings page to also randomize upgrade materials, Estus shards, and consumables.
-Another option is available to randomize the level of the generated weapons (from +0 to +10/+5).
+Items that can be picked up from static corpses, taken from chests, or earned from defeating enemies or NPCs can be
+randomized. Common pickups like titanite shards or firebombs can be randomized as "progressive" items. That is, the
+location "Titanite Shard #5" is the fifth titanite shard you pick up, no matter where it was from. This is also what
+happens when you randomize Estus Shards and Undead Bone Shards.
-To beat the game, you need to collect the 4 "Cinders of a Lord" randomized in the multiworld
-and kill the final boss "Soul of Cinder."
+It's also possible to randomize the upgrade level of weapons and shields as well as their infusions (if they can have
+one). Additionally, there are settings that can make the randomized experience more convenient or more interesting, such as
+removing weapon requirements or auto-equipping whatever equipment you most recently received.
+
+The goal is to find the four "Cinders of a Lord" items randomized into the multiworld and defeat the Soul of Cinder.
## What Dark Souls III items can appear in other players' worlds?
-Every unique item from Dark Souls III can appear in other player's worlds, such as a piece of armor, upgraded weapons,
-or key items.
+Practically anything can be found in other worlds including pieces of armor, upgraded weapons, key items, consumables,
+spells, upgrade materials, etc...
## What does another world's item look like in Dark Souls III?
-In Dark Souls III, items which need to be sent to other worlds appear as a Prism Stone.
+In Dark Souls III, items which are sent to other worlds appear as Prism Stones.
From 72b44be41c5b35205fcb2d0c40085a9a91c7463f Mon Sep 17 00:00:00 2001
From: Fabian Dill
Date: Sun, 10 Sep 2023 07:19:40 +0200
Subject: [PATCH 009/144] SNIClient: fix /snes command if tree (#791)
---
SNIClient.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/SNIClient.py b/SNIClient.py
index 66d0b2ca9c..0909c61382 100644
--- a/SNIClient.py
+++ b/SNIClient.py
@@ -68,12 +68,11 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
options = snes_options.split()
num_options = len(options)
- if num_options > 0:
- snes_device_number = int(options[0])
-
if num_options > 1:
snes_address = options[0]
snes_device_number = int(options[1])
+ elif num_options > 0:
+ snes_device_number = int(options[0])
self.ctx.snes_reconnect_address = None
if self.ctx.snes_connect_task:
From e01eb4e00c6e99840f196a6b62535e7fbd95b1fa Mon Sep 17 00:00:00 2001
From: Doug Hoskisson
Date: Sun, 10 Sep 2023 14:03:22 -0700
Subject: [PATCH 010/144] Zillion: webhost config fix (#2145)
---
worlds/zillion/__init__.py | 4 ++--
worlds/zillion/config.py | 17 +++++++++++++++++
2 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py
index 2ea7ffdea5..7c927c10eb 100644
--- a/worlds/zillion/__init__.py
+++ b/worlds/zillion/__init__.py
@@ -10,6 +10,7 @@ import logging
from BaseClasses import ItemClassification, LocationProgressType, \
MultiWorld, Item, CollectionState, Entrance, Tutorial
+from .config import detect_test
from .logic import cs_to_zz_locs
from .region import ZillionLocation, ZillionRegion
from .options import ZillionStartChar, zillion_options, validate
@@ -145,8 +146,7 @@ class ZillionWorld(World):
self._item_counts = item_counts
- import __main__
- rom_dir_name = "" if "test" in __main__.__file__ else os.path.dirname(get_base_rom_path())
+ rom_dir_name = "" if detect_test() else os.path.dirname(get_base_rom_path())
with redirect_stdout(self.lsi): # type: ignore
self.zz_system.make_patcher(rom_dir_name)
self.zz_system.make_randomizer(zz_op)
diff --git a/worlds/zillion/config.py b/worlds/zillion/config.py
index ca02f9a99f..db61d0c453 100644
--- a/worlds/zillion/config.py
+++ b/worlds/zillion/config.py
@@ -2,3 +2,20 @@ import os
base_id = 8675309
zillion_map = os.path.join(os.path.dirname(__file__), "empty-zillion-map-row-col-labels-281.png")
+
+
+def detect_test() -> bool:
+ """
+ Parts of generation that are in unit tests need the rom.
+ This is to detect whether we are running unit tests
+ so we can work around the need for the rom.
+ """
+ import __main__
+ try:
+ if "test" in __main__.__file__:
+ return True
+ except AttributeError:
+ # In some environments, __main__ doesn't have __file__
+ # We'll assume that's not unit tests.
+ pass
+ return False
From fbd64651e48cd1bd41436eec0fc8cf1726f308b2 Mon Sep 17 00:00:00 2001
From: Remy Jette
Date: Sun, 10 Sep 2023 14:24:09 -0700
Subject: [PATCH 011/144] Pokemon RB: Fix typo on the game info page (#2142)
Thanks Shiny for pointing it out https://discord.com/channels/731205301247803413/1043592720603693167/1147300361883893790
---
worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md
index 5350541827..daefd6b2f7 100644
--- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md
+++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md
@@ -20,7 +20,7 @@ Many baseline changes are made to the game, including:
* PC item storage increased to 64 slots (up from 50).
* You can hold B to run (or bike extra fast!).
* You can hold select while talking to a trainer to re-battle them.
-* You can select "Pallet Warp" below the "Continue" option to warp to Pallet Towna s you load your save.
+* You can select "Pallet Warp" below the "Continue" option to warp to Pallet Town as you load your save.
* Mew can be encountered at the S.S. Anne dock truck. This can be randomized depending on your settings.
* The S.S. Anne will never depart.
* Seafoam Islands entrances are swapped. This means you need Strength to travel through from Cinnabar Island to Fuchsia
From 8649b157873b26ea9bac37b8317529f7feb73a4c Mon Sep 17 00:00:00 2001
From: Trevor L <80716066+TRPG0@users.noreply.github.com>
Date: Sun, 10 Sep 2023 15:24:33 -0600
Subject: [PATCH 012/144] Blasphemous: Add missing logic (#2165)
* Blasphemous: Set rules for events later
* Blasphemous: More misc logic fixes
* Update worlds/blasphemous/Rules.py
Co-authored-by: Fabian Dill
* Update worlds/blasphemous/Rules.py
Co-authored-by: Fabian Dill
* Blasphemous: Some cleanup
* Blasphemous: Add missing logic
---------
Co-authored-by: Fabian Dill
---
worlds/blasphemous/Rules.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py
index 2ef36c575e..277a1b15dc 100644
--- a/worlds/blasphemous/Rules.py
+++ b/worlds/blasphemous/Rules.py
@@ -2451,6 +2451,8 @@ def rules(blasphemousworld):
# Items
set_rule(world.get_location("PotSS: 4th meeting with Redento", player),
lambda state: redento(state, blasphemousworld, player, 4))
+ set_rule(world.get_location("PotSS: Amanecida of the Chiselled Steel", player),
+ lambda state: can_beat_boss(state, "Patio", logic, player))
# No doors
From 6c844750aede98811c834e0e407d99ff7348dbf4 Mon Sep 17 00:00:00 2001
From: blastron
Date: Sun, 10 Sep 2023 14:29:42 -0700
Subject: [PATCH 013/144] Witness: fix items being modified by other slots
(#2161)
---
worlds/witness/items.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/worlds/witness/items.py b/worlds/witness/items.py
index 7e083534c9..82c79047f3 100644
--- a/worlds/witness/items.py
+++ b/worlds/witness/items.py
@@ -152,7 +152,7 @@ class WitnessPlayerItems:
"""
Returns the list of items that must be in the pool for the game to successfully generate.
"""
- return self._mandatory_items
+ return self._mandatory_items.copy()
def get_filler_items(self, quantity: int) -> Dict[str, int]:
"""
From 5eef7a34d3de1b1a82607d7460abb3bd0119e828 Mon Sep 17 00:00:00 2001
From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Date: Sun, 10 Sep 2023 23:34:20 +0200
Subject: [PATCH 014/144] The Witness: Fix Expert Tutorial Gate Close (#2164)
---
worlds/witness/WitnessLogicExpert.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/worlds/witness/WitnessLogicExpert.txt b/worlds/witness/WitnessLogicExpert.txt
index a55c22441e..b373c7417c 100644
--- a/worlds/witness/WitnessLogicExpert.txt
+++ b/worlds/witness/WitnessLogicExpert.txt
@@ -14,7 +14,7 @@ Tutorial (Tutorial) - Outside Tutorial - True:
158005 - 0x0A3B5 (Back Left) - True - Dots & Full Dots
158006 - 0x0A3B2 (Back Right) - True - Dots & Full Dots
158007 - 0x03629 (Gate Open) - 0x002C2 - Symmetry & Dots
-158008 - 0x03505 (Gate Close) - 0x2FAF6 & 0x03629 - False
+158008 - 0x03505 (Gate Close) - 0x2FAF6 & 0x03629 - True
158009 - 0x0C335 (Pillar) - True - Triangles
158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots
159512 - 0x33530 (Cloud EP) - True - True
From 0e21a3e121f020bbd2b44d71de199dfd8d07cfde Mon Sep 17 00:00:00 2001
From: Alchav <59858495+Alchav@users.noreply.github.com>
Date: Sun, 10 Sep 2023 17:38:56 -0400
Subject: [PATCH 015/144] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Fix=20broken=20op?=
=?UTF-8?q?tions=20(#2162)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
worlds/pokemon_rb/__init__.py | 2 +-
worlds/pokemon_rb/logic.py | 2 +-
worlds/pokemon_rb/rom.py | 25 +++++++++++++------------
3 files changed, 15 insertions(+), 14 deletions(-)
diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py
index 64f62adddb..3d6e463251 100644
--- a/worlds/pokemon_rb/__init__.py
+++ b/worlds/pokemon_rb/__init__.py
@@ -138,7 +138,7 @@ class PokemonRedBlueWorld(World):
if self.multiworld.key_items_only[self.player]:
self.multiworld.trainersanity[self.player] = self.multiworld.trainersanity[self.player].from_text("off")
- self.multiworld.dexsanity[self.player] = self.multiworld.dexsanity[self.player].from_text("false")
+ self.multiworld.dexsanity[self.player].value = 0
self.multiworld.randomize_hidden_items[self.player] = \
self.multiworld.randomize_hidden_items[self.player].from_text("off")
diff --git a/worlds/pokemon_rb/logic.py b/worlds/pokemon_rb/logic.py
index 87398c7267..cbe28e0ddb 100644
--- a/worlds/pokemon_rb/logic.py
+++ b/worlds/pokemon_rb/logic.py
@@ -53,7 +53,7 @@ def has_key_items(state, count, player):
"Hideout Key", "Card Key 2F", "Card Key 3F", "Card Key 4F", "Card Key 5F",
"Card Key 6F", "Card Key 7F", "Card Key 8F", "Card Key 9F", "Card Key 10F",
"Card Key 11F", "Exp. All", "Fire Stone", "Thunder Stone", "Water Stone",
- "Leaf Stone"] if state.has(item, player)])
+ "Leaf Stone", "Moon Stone"] if state.has(item, player)])
+ min(state.count("Progressive Card Key", player), 10))
return key_items >= count
diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py
index 0757d33435..4b191d9176 100644
--- a/worlds/pokemon_rb/rom.py
+++ b/worlds/pokemon_rb/rom.py
@@ -238,18 +238,19 @@ def generate_output(self, output_directory: str):
data[address] = 0 if "Elevator" in connected_map_name else warp_to_ids[i]
data[address + 1] = map_ids[connected_map_name]
- for i, gym_leader in enumerate(("Pewter Gym - Brock TM", "Cerulean Gym - Misty TM",
- "Vermilion Gym - Lt. Surge TM", "Celadon Gym - Erika TM",
- "Fuchsia Gym - Koga TM", "Saffron Gym - Sabrina TM",
- "Cinnabar Gym - Blaine TM", "Viridian Gym - Giovanni TM")):
- item_name = self.multiworld.get_location(gym_leader, self.player).item.name
- if item_name.startswith("TM"):
- try:
- tm = int(item_name[2:4])
- move = poke_data.moves[self.local_tms[tm - 1]]["id"]
- data[rom_addresses["Gym_Leader_Moves"] + (2 * i)] = move
- except KeyError:
- pass
+ if not self.multiworld.key_items_only[self.player]:
+ for i, gym_leader in enumerate(("Pewter Gym - Brock TM", "Cerulean Gym - Misty TM",
+ "Vermilion Gym - Lt. Surge TM", "Celadon Gym - Erika TM",
+ "Fuchsia Gym - Koga TM", "Saffron Gym - Sabrina TM",
+ "Cinnabar Gym - Blaine TM", "Viridian Gym - Giovanni TM")):
+ item_name = self.multiworld.get_location(gym_leader, self.player).item.name
+ if item_name.startswith("TM"):
+ try:
+ tm = int(item_name[2:4])
+ move = poke_data.moves[self.local_tms[tm - 1]]["id"]
+ data[rom_addresses["Gym_Leader_Moves"] + (2 * i)] = move
+ except KeyError:
+ pass
def set_trade_mon(address, loc):
mon = self.multiworld.get_location(loc, self.player).item.name
From 3e95ccd06c8955762c20b58555961244fbc09ffe Mon Sep 17 00:00:00 2001
From: Trevor L <80716066+TRPG0@users.noreply.github.com>
Date: Sun, 10 Sep 2023 16:04:57 -0600
Subject: [PATCH 016/144] Blasphemous: Fixed Amanecidas not requiring Petrified
Bell (#2166)
---
worlds/blasphemous/Rules.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py
index 277a1b15dc..248ff645bc 100644
--- a/worlds/blasphemous/Rules.py
+++ b/worlds/blasphemous/Rules.py
@@ -367,25 +367,25 @@ def can_beat_boss(state: CollectionState, boss: str, logic: int, player: int) ->
elif boss == "Graveyard":
return (
has_boss_strength("amanecida")
- and state.has_all({"D01BZ07S01[Santos]", "D02Z03S23[E]", "D02Z02S14[W]", "Wall Climb Ability"}, player)
+ and state.has_all({"D01Z06S01[Santos]", "D02Z03S23[E]", "D02Z02S14[W]", "Wall Climb Ability"}, player)
)
elif boss == "Jondo":
return (
has_boss_strength("amanecida")
- and state.has("D01BZ07S01[Santos]", player)
+ and state.has("D01Z06S01[Santos]", player)
and state.has_any({"D20Z01S05[W]", "D20Z01S05[E]"}, player)
and state.has_any({"D03Z01S03[W]", "D03Z01S03[SW]"}, player)
)
elif boss == "Patio":
return (
has_boss_strength("amanecida")
- and state.has_all({"D01BZ07S01[Santos]", "D06Z01S18[E]"}, player)
+ and state.has_all({"D01Z06S01[Santos]", "D06Z01S18[E]"}, player)
and state.has_any({"D04Z01S04[W]", "D04Z01S04[E]", "D04Z01S04[Cherubs]"}, player)
)
elif boss == "Wall":
return (
has_boss_strength("amanecida")
- and state.has_all({"D01BZ07S01[Santos]", "D09BZ01S01[Cell24]"}, player)
+ and state.has_all({"D01Z06S01[Santos]", "D09BZ01S01[Cell24]"}, player)
and state.has_any({"D09Z01S01[W]", "D09Z01S01[E]"}, player)
)
elif boss == "Hall":
From 3d9837678c9a4989fa88da1f97109995e07e0812 Mon Sep 17 00:00:00 2001
From: Rob B
Date: Sun, 10 Sep 2023 17:13:39 -0500
Subject: [PATCH 017/144] Factorio: better Technology Tree Information
description (#2121)
* Fix typo in Factorio options tooltip
* Fix typo, add details
* Apply code review suggestion
It doesn't let me apply more than one change to the same line in a batch.
Co-authored-by: Scipio Wright
* Apply code review suggestion from @nicholassaylor
It doesn't let me apply more than one change to the same line in a batch.
---------
Co-authored-by: Scipio Wright
---
worlds/factorio/Options.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py
index 0331c2d013..2b579658fc 100644
--- a/worlds/factorio/Options.py
+++ b/worlds/factorio/Options.py
@@ -146,8 +146,8 @@ class TechTreeLayout(Choice):
class TechTreeInformation(Choice):
"""How much information should be displayed in the tech tree.
- None: No indication what a research unlocks
- Advancement: Indicators which researches unlock items that are considered logical advancements
+ None: No indication of what a research unlocks.
+ Advancement: Indicates if a research unlocks an item that is considered logical advancement, but not who it is for.
Full: Labels with exact names and recipients of unlocked items; all researches are prefilled into the !hint command.
"""
display_name = "Technology Tree Information"
From 57c13ff2732691c0a44b34e69742cfbdc2ad38f8 Mon Sep 17 00:00:00 2001
From: Remy Jette
Date: Mon, 11 Sep 2023 13:57:14 -0700
Subject: [PATCH 018/144] WebHost: Support multi-select during check/generate
file upload (#2138)
* Support multi-select during check/generate file upload
This will allow the user to select multiple YAML files via Shift-Click
or Control-Click in their browser when generating a game via the site
instead of having to zip them locally first.
* Update generate.html: File -> File(s)
* Change check.html button text to "Upload File(s)" to match generate.html
---
WebHostLib/check.py | 47 ++++++++++++++++--------------
WebHostLib/generate.py | 4 +--
WebHostLib/templates/check.html | 4 +--
WebHostLib/templates/generate.html | 4 +--
4 files changed, 31 insertions(+), 28 deletions(-)
diff --git a/WebHostLib/check.py b/WebHostLib/check.py
index 0c1e090dbe..c5dfd9f556 100644
--- a/WebHostLib/check.py
+++ b/WebHostLib/check.py
@@ -24,8 +24,8 @@ def check():
if 'file' not in request.files:
flash('No file part')
else:
- file = request.files['file']
- options = get_yaml_data(file)
+ files = request.files.getlist('file')
+ options = get_yaml_data(files)
if isinstance(options, str):
flash(options)
else:
@@ -39,30 +39,33 @@ def mysterycheck():
return redirect(url_for("check"), 301)
-def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]:
+def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]:
options = {}
- # if user does not select file, browser also
- # submit an empty part without filename
- if file.filename == '':
- return 'No selected file'
- elif file and allowed_file(file.filename):
- if file.filename.endswith(".zip"):
+ for file in files:
+ # if user does not select file, browser also
+ # submit an empty part without filename
+ if file.filename == '':
+ return 'No selected file'
+ elif file.filename in options:
+ return f'Conflicting files named {file.filename} submitted'
+ elif file and allowed_file(file.filename):
+ if file.filename.endswith(".zip"):
- with zipfile.ZipFile(file, 'r') as zfile:
- infolist = zfile.infolist()
+ with zipfile.ZipFile(file, 'r') as zfile:
+ infolist = zfile.infolist()
- if any(file.filename.endswith(".archipelago") for file in infolist):
- return Markup("Error: Your .zip file contains an .archipelago file. "
- 'Did you mean to host a game?')
+ if any(file.filename.endswith(".archipelago") for file in infolist):
+ return Markup("Error: Your .zip file contains an .archipelago file. "
+ 'Did you mean to host a game?')
- for file in infolist:
- if file.filename.endswith(banned_zip_contents):
- return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
- "Your file was deleted."
- elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
- options[file.filename] = zfile.open(file, "r").read()
- else:
- options = {file.filename: file.read()}
+ for file in infolist:
+ if file.filename.endswith(banned_zip_contents):
+ return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
+ "Your file was deleted."
+ elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
+ options[file.filename] = zfile.open(file, "r").read()
+ else:
+ options[file.filename] = file.read()
if not options:
return "Did not find a .yaml file to process."
return options
diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py
index 91d7594a1f..ddcc5ffb6c 100644
--- a/WebHostLib/generate.py
+++ b/WebHostLib/generate.py
@@ -64,8 +64,8 @@ def generate(race=False):
if 'file' not in request.files:
flash('No file part')
else:
- file = request.files['file']
- options = get_yaml_data(file)
+ files = request.files.getlist('file')
+ options = get_yaml_data(files)
if isinstance(options, str):
flash(options)
else:
diff --git a/WebHostLib/templates/check.html b/WebHostLib/templates/check.html
index 04b51340b5..8a3da7db47 100644
--- a/WebHostLib/templates/check.html
+++ b/WebHostLib/templates/check.html
@@ -17,9 +17,9 @@
-
+
diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html
index dd25a90804..33f8dbc09e 100644
--- a/WebHostLib/templates/generate.html
+++ b/WebHostLib/templates/generate.html
@@ -203,10 +203,10 @@ Warning: playthrough can take a significant amount of time for larger multiworld
-
+
-
+
From 1756a30accb1b2fd8e434ab8a75c11db7ef024d4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fri=C3=B0berg?=
Date: Mon, 11 Sep 2023 21:17:11 +0000
Subject: [PATCH 019/144] WebHost: Clean up the exported yaml in weighted
settings (#2167)
* Trim output yaml in weighted options
Remove options that have only one possible outcome as well as empty arrays, when building yaml.
* fix quotes
---
WebHostLib/static/assets/weighted-settings.js | 33 ++++++++++++++++++-
1 file changed, 32 insertions(+), 1 deletion(-)
diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js
index 6e86d470f0..07157cb579 100644
--- a/WebHostLib/static/assets/weighted-settings.js
+++ b/WebHostLib/static/assets/weighted-settings.js
@@ -1134,8 +1134,8 @@ const validateSettings = () => {
return;
}
- // Remove any disabled options
Object.keys(settings[game]).forEach((setting) => {
+ // Remove any disabled options
Object.keys(settings[game][setting]).forEach((option) => {
if (settings[game][setting][option] === 0) {
delete settings[game][setting][option];
@@ -1149,6 +1149,32 @@ const validateSettings = () => {
) {
errorMessage = `${game} // ${setting} has no values above zero!`;
}
+
+ // Remove weights from options with only one possibility
+ if (
+ Object.keys(settings[game][setting]).length === 1 &&
+ !Array.isArray(settings[game][setting]) &&
+ setting !== 'start_inventory'
+ ) {
+ settings[game][setting] = Object.keys(settings[game][setting])[0];
+ }
+
+ // Remove empty arrays
+ else if (
+ ['exclude_locations', 'priority_locations', 'local_items',
+ 'non_local_items', 'start_hints', 'start_location_hints'].includes(setting) &&
+ settings[game][setting].length === 0
+ ) {
+ delete settings[game][setting];
+ }
+
+ // Remove empty start inventory
+ else if (
+ setting === 'start_inventory' &&
+ Object.keys(settings[game]['start_inventory']).length === 0
+ ) {
+ delete settings[game]['start_inventory'];
+ }
});
});
@@ -1156,6 +1182,11 @@ const validateSettings = () => {
errorMessage = 'You have not chosen a game to play!';
}
+ // Remove weights if there is only one game
+ else if (Object.keys(settings.game).length === 1) {
+ settings.game = Object.keys(settings.game)[0];
+ }
+
// If an error occurred, alert the user and do not export the file
if (errorMessage) {
userMessage.innerText = errorMessage;
From c3cfbf8e1cc4304659a28c1b2519070a43a3f809 Mon Sep 17 00:00:00 2001
From: Seldom <38388947+Seldom-SE@users.noreply.github.com>
Date: Thu, 14 Sep 2023 00:46:29 -0700
Subject: [PATCH 020/144] Terraria: Add the rest of the settings to slot data
(#2116)
* Add the rest of the Terraria settings to slot data
* Update worlds/terraria/__init__.py
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
---------
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
---
worlds/terraria/__init__.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/worlds/terraria/__init__.py b/worlds/terraria/__init__.py
index a56f47608b..a8c823bcb8 100644
--- a/worlds/terraria/__init__.py
+++ b/worlds/terraria/__init__.py
@@ -338,5 +338,7 @@ class TerrariaWorld(World):
def fill_slot_data(self) -> Dict[str, object]:
return {
"goal": list(self.goal_locations),
+ "achievements": self.multiworld.achievements[self.player].value,
+ "fill_extra_checks_with": self.multiworld.fill_extra_checks_with[self.player].value,
"deathlink": bool(self.multiworld.death_link[self.player]),
}
From 8ee743ac8a1e782e7bdb36ccff4c9078a4d77cf3 Mon Sep 17 00:00:00 2001
From: lordlou <87331798+lordlou@users.noreply.github.com>
Date: Thu, 14 Sep 2023 15:49:11 -0400
Subject: [PATCH 021/144] SM: 0.4.2 fixes (#2175)
## What is this fixing or adding?
- fixed failing generation with disabled layout patch by moving door_indicators_plms.ips to AP instead of the base patch (reported at https://discord.com/channels/731205301247803413/1149509811529072751/1149509811529072751)
(part of the fix is in the Basepatch with this commit https://github.com/lordlou/SMBasepatch/commit/46bbda980cd6eec69c59c71355bd3f975b827456)
- fixed broken map data saving when using fast_save (reported at https://discord.com/channels/731205301247803413/1138163133089849344/1138163133089849344)
(part of the fix is in the Basepatch with this commit https://github.com/lordlou/SMBasepatch/commit/54a82774c9287274bdee70da8401d14d8d3720b9)
---
.../multiworld-basepatch.ips | Bin 19126 -> 19144 bytes
.../data/SMBasepatch_prebuilt/multiworld.sym | 315 +++++++++---------
.../sm-basepatch-symbols.json | 5 +-
.../SMBasepatch_prebuilt/variapatches.ips | Bin 35247 -> 34458 bytes
worlds/sm/variaRandomizer/rom/rompatcher.py | 4 +-
5 files changed, 167 insertions(+), 157 deletions(-)
diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips
index 0455364d8a7b68251975be054fbb7e8ffbde4791..67863bb9f00228aee09d1c8257bf41cbd17e8a90 100644
GIT binary patch
delta 62
zcmdlsmGQ(>#t9P`uTGp8q_aMVvrdVjp%FxDGIOokuie;~8OFFO!$Xtd|Lgfo3xSu0g#XZ!;z6v8Yuw(>y4iWXh|YlqxW)J3YG{
zjA{;}l9RN(ifM0G%Gu5bgKiyzUM@_lUogsmhTVXMTt*7?8T7tMKR_dnY0ZM}gT@=0sW8nGO;i?E+s
zwPb+~NUhjA&^gfmV|j4eMA5WTuo|b$gPwu5J}vp6k1b+3{@mZwe!+gXtPNs|1=AW-
zTCsJ|dC
Date: Thu, 14 Sep 2023 15:49:57 -0400
Subject: [PATCH 022/144] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20More=20tracker=20?=
=?UTF-8?q?slot=20data=20(#2174)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
worlds/pokemon_rb/__init__.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py
index 3d6e463251..2c70f28416 100644
--- a/worlds/pokemon_rb/__init__.py
+++ b/worlds/pokemon_rb/__init__.py
@@ -717,6 +717,15 @@ class PokemonRedBlueWorld(World):
"death_link": self.multiworld.death_link[self.player].value,
"prizesanity": self.multiworld.prizesanity[self.player].value,
"key_items_only": self.multiworld.key_items_only[self.player].value,
+ "poke_doll_skip": self.multiworld.poke_doll_skip[self.player].value,
+ "bicycle_gate_skips": self.multiworld.bicycle_gate_skips[self.player].value,
+ "stonesanity": self.multiworld.stonesanity[self.player].value,
+ "door_shuffle": self.multiworld.door_shuffle[self.player].value,
+ "warp_tile_shuffle": self.multiworld.warp_tile_shuffle[self.player].value,
+ "dark_rock_tunnel_logic": self.multiworld.dark_rock_tunnel_logic[self.player].value,
+ "split_card_key": self.multiworld.split_card_key[self.player].value,
+ "all_elevators_locked": self.multiworld.all_elevators_locked[self.player].value,
+
}
From fdac50523b58afc7aaf660b67f9411ddc1f84bde Mon Sep 17 00:00:00 2001
From: agilbert1412
Date: Thu, 14 Sep 2023 17:56:13 -0400
Subject: [PATCH 023/144] Stardew Valley: Added missing logic rules for dating
and marriage (#2160)
* - Added missing logic rules where, to earn hearts above 8 and 10, you need access to dating and marriage respectively.
* - Slight cleanup based on Black Sliver's suggestion
---
worlds/stardew_valley/logic.py | 24 +++++++---
worlds/stardew_valley/test/TestRules.py | 60 +++++++++++++++++++++++++
2 files changed, 78 insertions(+), 6 deletions(-)
diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py
index 46580d549a..00b60696a9 100644
--- a/worlds/stardew_valley/logic.py
+++ b/worlds/stardew_valley/logic.py
@@ -1043,11 +1043,12 @@ class StardewLogic:
def has_relationship(self, npc: str, hearts: int = 1) -> StardewRule:
if hearts <= 0:
return True_()
- if self.options[options.Friendsanity] == options.Friendsanity.option_none:
+ friendsanity = self.options[options.Friendsanity]
+ if friendsanity == options.Friendsanity.option_none:
return self.can_earn_relationship(npc, hearts)
if npc not in all_villagers_by_name:
if npc == NPC.pet:
- if self.options[options.Friendsanity] == options.Friendsanity.option_bachelors:
+ if friendsanity == options.Friendsanity.option_bachelors:
return self.can_befriend_pet(hearts)
return self.received_hearts(NPC.pet, hearts)
if npc == Generic.any or npc == Generic.bachelor:
@@ -1077,12 +1078,12 @@ class StardewLogic:
if not self.npc_is_in_current_slot(npc):
return True_()
villager = all_villagers_by_name[npc]
- if self.options[options.Friendsanity] == options.Friendsanity.option_bachelors and not villager.bachelor:
+ if friendsanity == options.Friendsanity.option_bachelors and not villager.bachelor:
return self.can_earn_relationship(npc, hearts)
- if self.options[options.Friendsanity] == options.Friendsanity.option_starting_npcs and not villager.available:
+ if friendsanity == options.Friendsanity.option_starting_npcs and not villager.available:
return self.can_earn_relationship(npc, hearts)
- if self.options[
- options.Friendsanity] != options.Friendsanity.option_all_with_marriage and villager.bachelor and hearts > 8:
+ is_capped_at_8 = villager.bachelor and friendsanity != options.Friendsanity.option_all_with_marriage
+ if is_capped_at_8 and hearts > 8:
return self.received_hearts(villager, 8) & self.can_earn_relationship(npc, hearts)
return self.received_hearts(villager, hearts)
@@ -1136,11 +1137,22 @@ class StardewLogic:
rule_if_birthday = self.has_season(villager.birthday) & self.has_any_universal_love() & self.has_lived_months(hearts // 2)
rule_if_not_birthday = self.has_lived_months(hearts)
earn_rule = self.can_meet(npc) & (rule_if_birthday | rule_if_not_birthday)
+ if villager.bachelor:
+ if hearts > 8:
+ earn_rule = earn_rule & self.can_date(npc)
+ if hearts > 10:
+ earn_rule = earn_rule & self.can_marry(npc)
else:
earn_rule = self.has_lived_months(min(hearts // 2, 8))
return previous_heart_rule & earn_rule
+ def can_date(self, npc: str) -> StardewRule:
+ return self.has_relationship(npc, 8) & self.has(Gift.bouquet)
+
+ def can_marry(self, npc: str) -> StardewRule:
+ return self.has_relationship(npc, 10) & self.has(Gift.mermaid_pendant)
+
def can_befriend_pet(self, hearts: int):
if hearts <= 0:
return True_()
diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py
index 8556dac1d8..0847d8a63b 100644
--- a/worlds/stardew_valley/test/TestRules.py
+++ b/worlds/stardew_valley/test/TestRules.py
@@ -444,3 +444,63 @@ def collect_all_except(multiworld, item_to_not_collect: str):
for item in multiworld.get_items():
if item.name != item_to_not_collect:
multiworld.state.collect(item)
+
+
+class TestFriendsanityDatingRules(SVTestBase):
+ options = {
+ options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized_not_winter,
+ options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage,
+ options.FriendsanityHeartSize.internal_name: 3
+ }
+
+ def test_earning_dating_heart_requires_dating(self):
+ month_name = "Month End"
+ for i in range(12):
+ month_item = self.world.create_item(month_name)
+ self.multiworld.state.collect(month_item, event=True)
+ self.multiworld.state.collect(self.world.create_item("Beach Bridge"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Progressive House"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Adventurer's Guild"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Galaxy Hammer"), event=False)
+ for i in range(3):
+ self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Progressive Barn"), event=False)
+ for i in range(10):
+ self.multiworld.state.collect(self.world.create_item("Foraging Level"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Farming Level"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Mining Level"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Combat Level"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Progressive Mine Elevator"), event=False)
+ self.multiworld.state.collect(self.world.create_item("Progressive Mine Elevator"), event=False)
+
+ npc = "Abigail"
+ heart_name = f"{npc} <3"
+ step = 3
+
+ self.assert_can_reach_heart_up_to(npc, 3, step)
+ self.multiworld.state.collect(self.world.create_item(heart_name), event=False)
+ self.assert_can_reach_heart_up_to(npc, 6, step)
+ self.multiworld.state.collect(self.world.create_item(heart_name), event=False)
+ self.assert_can_reach_heart_up_to(npc, 8, step)
+ self.multiworld.state.collect(self.world.create_item(heart_name), event=False)
+ self.assert_can_reach_heart_up_to(npc, 10, step)
+ self.multiworld.state.collect(self.world.create_item(heart_name), event=False)
+ self.assert_can_reach_heart_up_to(npc, 14, step)
+
+ def assert_can_reach_heart_up_to(self, npc: str, max_reachable: int, step: int):
+ prefix = "Friendsanity: "
+ suffix = " <3"
+ for i in range(1, max_reachable + 1):
+ if i % step != 0 and i != 14:
+ continue
+ location = f"{prefix}{npc} {i}{suffix}"
+ can_reach = self.world.logic.can_reach_location(location)(self.multiworld.state)
+ self.assertTrue(can_reach, f"Should be able to earn relationship up to {i} hearts")
+ for i in range(max_reachable + 1, 14 + 1):
+ if i % step != 0 and i != 14:
+ continue
+ location = f"{prefix}{npc} {i}{suffix}"
+ can_reach = self.world.logic.can_reach_location(location)(self.multiworld.state)
+ self.assertFalse(can_reach, f"Should not be able to earn relationship up to {i} hearts")
+
From 47cf3e06c0bb74355da1a0e627f4b673454a1eac Mon Sep 17 00:00:00 2001
From: BadMagic100
Date: Thu, 14 Sep 2023 14:57:36 -0700
Subject: [PATCH 024/144] Hollow Knight: Update outdated setup documentation
(#2171)
* Hollow Knight: Update outdated setup documentation
* Update a reference from Scarab to Scarab+
Co-authored-by: kindasneaki
* Fix numbering
---------
Co-authored-by: kindasneaki
---
worlds/hk/docs/setup_en.md | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/worlds/hk/docs/setup_en.md b/worlds/hk/docs/setup_en.md
index e25e6bc4ac..adf975ff51 100644
--- a/worlds/hk/docs/setup_en.md
+++ b/worlds/hk/docs/setup_en.md
@@ -4,13 +4,10 @@
* Download and unzip the Scarab+ Mod Manager from the [Scarab+ website](https://themulhima.github.io/Scarab/).
* A legal copy of Hollow Knight.
-## Optional Software
-* Archipelago Map Mod from Scarab+
- * Ensure that both RandoMapMod and MapChanger are uninstalled or disabled as they are incompatible with Archipelago Map Mod.
-
-## Installing the Archipelago Mod using Scarab
+## Installing the Archipelago Mod using Scarab+
1. Launch Scarab+ and ensure it locates your Hollow Knight installation directory.
2. Click the "Install" button near the "Archipelago" mod entry.
+ * If desired, also install "Archipelago Map Mod" to use as an in-game tracker.
3. Launch the game, you're all set!
### What to do if Scarab+ fails to find your XBox Game Pass installation directory
From 648d682add36ae4bae939a9252972e35e03eb8e1 Mon Sep 17 00:00:00 2001
From: Ziktofel
Date: Fri, 15 Sep 2023 02:22:10 +0200
Subject: [PATCH 025/144] SC2 WoL - Mod, Item and Location update (#2113)
Migrates SC2 WoL world to the new mod with new items and locations. The new mod has a different architecture making it more future proof (with planned adding of other campaigns). Also gets rid of several old bugs
Adds new short game formats intended for sync games (Tiny Grid, Mini Gauntlet). The final mission isn't decided by campaign length anymore but it's configurable instead. Allow excluding missions for Vanilla Shuffled, corrected some documentation.
NOTE: This is a squashed commit with Salz' HotS excluded (not ready for the release and I plan multi-campaign instead)
---------
Co-authored-by: Matthew
---
Starcraft2Client.py | 1050 +-------------
.../static/icons/sc2/SC2_Lab_BioSteel_L1.png | Bin 0 -> 5945 bytes
.../static/icons/sc2/SC2_Lab_BioSteel_L2.png | Bin 0 -> 6699 bytes
.../static/icons/sc2/advanceballistics.png | Bin 0 -> 11624 bytes
.../static/icons/sc2/autoturretblackops.png | Bin 0 -> 8834 bytes
.../static/icons/sc2/biomechanicaldrone.png | Bin 0 -> 6999 bytes
.../static/icons/sc2/burstcapacitors.png | Bin 0 -> 2579 bytes
.../icons/sc2/crossspectrumdampeners.png | Bin 0 -> 5344 bytes
.../static/static/icons/sc2/cyclone.png | Bin 0 -> 8524 bytes
.../static/icons/sc2/cyclonerangeupgrade.png | Bin 0 -> 12682 bytes
.../static/static/icons/sc2/drillingclaws.png | Bin 0 -> 8451 bytes
.../static/icons/sc2/emergencythrusters.png | Bin 0 -> 6796 bytes
.../static/icons/sc2/hellionbattlemode.png | Bin 0 -> 8210 bytes
.../icons/sc2/high-explosive-spidermine.png | Bin 0 -> 14334 bytes
.../static/icons/sc2/hyperflightrotors.png | Bin 0 -> 14285 bytes
.../static/static/icons/sc2/hyperfluxor.png | Bin 0 -> 9261 bytes
.../static/static/icons/sc2/impalerrounds.png | Bin 0 -> 4347 bytes
.../static/icons/sc2/improvedburstlaser.png | Bin 0 -> 11115 bytes
.../static/icons/sc2/improvedsiegemode.png | Bin 0 -> 14293 bytes
.../static/icons/sc2/interferencematrix.png | Bin 0 -> 10537 bytes
.../icons/sc2/internalizedtechmodule.png | Bin 0 -> 16425 bytes
.../static/static/icons/sc2/jotunboosters.png | Bin 0 -> 5740 bytes
.../static/static/icons/sc2/jumpjets.png | Bin 0 -> 17881 bytes
.../static/icons/sc2/lasertargetingsystem.png | Bin 0 -> 14802 bytes
.../static/static/icons/sc2/liberator.png | Bin 0 -> 9012 bytes
.../static/static/icons/sc2/lockdown.png | Bin 0 -> 8289 bytes
.../static/icons/sc2/magfieldaccelerator.png | Bin 0 -> 11459 bytes
.../static/icons/sc2/magrailmunitions.png | Bin 0 -> 19193 bytes
.../icons/sc2/medivacemergencythrusters.png | Bin 0 -> 8954 bytes
.../icons/sc2/neosteelfortifiedarmor.png | Bin 0 -> 13032 bytes
.../static/static/icons/sc2/opticalflare.png | Bin 0 -> 11440 bytes
.../static/icons/sc2/optimizedlogistics.png | Bin 0 -> 13116 bytes
.../static/icons/sc2/reapercombatdrugs.png | Bin 0 -> 7102 bytes
.../static/static/icons/sc2/restoration.png | Bin 0 -> 7754 bytes
.../static/icons/sc2/ripwavemissiles.png | Bin 0 -> 13628 bytes
.../static/icons/sc2/shreddermissile.png | Bin 0 -> 9827 bytes
.../icons/sc2/siegetank-spidermines.png | Bin 0 -> 12764 bytes
.../static/icons/sc2/siegetankrange.png | Bin 0 -> 11833 bytes
.../static/icons/sc2/specialordance.png | Bin 0 -> 12992 bytes
.../static/static/icons/sc2/spidermine.png | Bin 0 -> 3872 bytes
.../static/icons/sc2/staticempblast.png | Bin 0 -> 12094 bytes
.../static/static/icons/sc2/superstimpack.png | Bin 0 -> 14901 bytes
.../static/icons/sc2/targetingoptics.png | Bin 0 -> 8431 bytes
.../static/icons/sc2/terran-cloak-color.png | Bin 0 -> 8134 bytes
.../static/icons/sc2/terran-emp-color.png | Bin 0 -> 7693 bytes
.../sc2/terrandefendermodestructureattack.png | Bin 0 -> 14017 bytes
.../static/static/icons/sc2/thorsiegemode.png | Bin 0 -> 11345 bytes
.../static/icons/sc2/transformationservos.png | Bin 0 -> 9215 bytes
.../static/static/icons/sc2/valkyrie.png | Bin 0 -> 7490 bytes
.../static/static/icons/sc2/warpjump.png | Bin 0 -> 8665 bytes
.../icons/sc2/widowmine-attackrange.png | Bin 0 -> 13367 bytes
.../icons/sc2/widowmine-deathblossom.png | Bin 0 -> 12946 bytes
.../static/static/icons/sc2/widowmine.png | Bin 0 -> 5671 bytes
.../static/icons/sc2/widowminehidden.png | Bin 0 -> 12777 bytes
WebHostLib/static/styles/sc2wolTracker.css | 6 +-
WebHostLib/templates/sc2wolTracker.html | 324 +++--
WebHostLib/tracker.py | 234 +++-
setup.py | 2 +-
worlds/_sc2common/bot/maps.py | 27 +-
worlds/sc2wol/Client.py | 1201 +++++++++++++++++
worlds/sc2wol/Items.py | 235 +++-
worlds/sc2wol/Locations.py | 490 +++++--
worlds/sc2wol/LogicMixin.py | 76 +-
worlds/sc2wol/MissionTables.py | 34 +-
worlds/sc2wol/Options.py | 283 +++-
worlds/sc2wol/PoolFilter.py | 155 ++-
worlds/sc2wol/Regions.py | 47 +-
worlds/sc2wol/__init__.py | 160 ++-
68 files changed, 2856 insertions(+), 1468 deletions(-)
create mode 100644 WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png
create mode 100644 WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png
create mode 100644 WebHostLib/static/static/icons/sc2/advanceballistics.png
create mode 100644 WebHostLib/static/static/icons/sc2/autoturretblackops.png
create mode 100644 WebHostLib/static/static/icons/sc2/biomechanicaldrone.png
create mode 100644 WebHostLib/static/static/icons/sc2/burstcapacitors.png
create mode 100644 WebHostLib/static/static/icons/sc2/crossspectrumdampeners.png
create mode 100644 WebHostLib/static/static/icons/sc2/cyclone.png
create mode 100644 WebHostLib/static/static/icons/sc2/cyclonerangeupgrade.png
create mode 100644 WebHostLib/static/static/icons/sc2/drillingclaws.png
create mode 100644 WebHostLib/static/static/icons/sc2/emergencythrusters.png
create mode 100644 WebHostLib/static/static/icons/sc2/hellionbattlemode.png
create mode 100644 WebHostLib/static/static/icons/sc2/high-explosive-spidermine.png
create mode 100644 WebHostLib/static/static/icons/sc2/hyperflightrotors.png
create mode 100644 WebHostLib/static/static/icons/sc2/hyperfluxor.png
create mode 100644 WebHostLib/static/static/icons/sc2/impalerrounds.png
create mode 100644 WebHostLib/static/static/icons/sc2/improvedburstlaser.png
create mode 100644 WebHostLib/static/static/icons/sc2/improvedsiegemode.png
create mode 100644 WebHostLib/static/static/icons/sc2/interferencematrix.png
create mode 100644 WebHostLib/static/static/icons/sc2/internalizedtechmodule.png
create mode 100644 WebHostLib/static/static/icons/sc2/jotunboosters.png
create mode 100644 WebHostLib/static/static/icons/sc2/jumpjets.png
create mode 100644 WebHostLib/static/static/icons/sc2/lasertargetingsystem.png
create mode 100644 WebHostLib/static/static/icons/sc2/liberator.png
create mode 100644 WebHostLib/static/static/icons/sc2/lockdown.png
create mode 100644 WebHostLib/static/static/icons/sc2/magfieldaccelerator.png
create mode 100644 WebHostLib/static/static/icons/sc2/magrailmunitions.png
create mode 100644 WebHostLib/static/static/icons/sc2/medivacemergencythrusters.png
create mode 100644 WebHostLib/static/static/icons/sc2/neosteelfortifiedarmor.png
create mode 100644 WebHostLib/static/static/icons/sc2/opticalflare.png
create mode 100644 WebHostLib/static/static/icons/sc2/optimizedlogistics.png
create mode 100644 WebHostLib/static/static/icons/sc2/reapercombatdrugs.png
create mode 100644 WebHostLib/static/static/icons/sc2/restoration.png
create mode 100644 WebHostLib/static/static/icons/sc2/ripwavemissiles.png
create mode 100644 WebHostLib/static/static/icons/sc2/shreddermissile.png
create mode 100644 WebHostLib/static/static/icons/sc2/siegetank-spidermines.png
create mode 100644 WebHostLib/static/static/icons/sc2/siegetankrange.png
create mode 100644 WebHostLib/static/static/icons/sc2/specialordance.png
create mode 100644 WebHostLib/static/static/icons/sc2/spidermine.png
create mode 100644 WebHostLib/static/static/icons/sc2/staticempblast.png
create mode 100644 WebHostLib/static/static/icons/sc2/superstimpack.png
create mode 100644 WebHostLib/static/static/icons/sc2/targetingoptics.png
create mode 100644 WebHostLib/static/static/icons/sc2/terran-cloak-color.png
create mode 100644 WebHostLib/static/static/icons/sc2/terran-emp-color.png
create mode 100644 WebHostLib/static/static/icons/sc2/terrandefendermodestructureattack.png
create mode 100644 WebHostLib/static/static/icons/sc2/thorsiegemode.png
create mode 100644 WebHostLib/static/static/icons/sc2/transformationservos.png
create mode 100644 WebHostLib/static/static/icons/sc2/valkyrie.png
create mode 100644 WebHostLib/static/static/icons/sc2/warpjump.png
create mode 100644 WebHostLib/static/static/icons/sc2/widowmine-attackrange.png
create mode 100644 WebHostLib/static/static/icons/sc2/widowmine-deathblossom.png
create mode 100644 WebHostLib/static/static/icons/sc2/widowmine.png
create mode 100644 WebHostLib/static/static/icons/sc2/widowminehidden.png
create mode 100644 worlds/sc2wol/Client.py
diff --git a/Starcraft2Client.py b/Starcraft2Client.py
index cdcdb39a0b..87b50d3506 100644
--- a/Starcraft2Client.py
+++ b/Starcraft2Client.py
@@ -1,1049 +1,11 @@
from __future__ import annotations
-import asyncio
-import copy
-import ctypes
-import logging
-import multiprocessing
-import os.path
-import re
-import sys
-import typing
-import queue
-import zipfile
-import io
-from pathlib import Path
+import ModuleUpdate
+ModuleUpdate.update()
-# CommonClient import first to trigger ModuleUpdater
-from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
-from Utils import init_logging, is_windows
+from worlds.sc2wol.Client import launch
+import Utils
if __name__ == "__main__":
- init_logging("SC2Client", exception_logger="Client")
-
-logger = logging.getLogger("Client")
-sc2_logger = logging.getLogger("Starcraft2")
-
-import nest_asyncio
-from worlds._sc2common import bot
-from worlds._sc2common.bot.data import Race
-from worlds._sc2common.bot.main import run_game
-from worlds._sc2common.bot.player import Bot
-from worlds.sc2wol import SC2WoLWorld
-from worlds.sc2wol.Items import lookup_id_to_name, item_table, ItemData, type_flaggroups
-from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
-from worlds.sc2wol.MissionTables import lookup_id_to_mission
-from worlds.sc2wol.Regions import MissionInfo
-
-import colorama
-from NetUtils import ClientStatus, NetworkItem, RawJSONtoTextParser
-from MultiServer import mark_raw
-
-nest_asyncio.apply()
-max_bonus: int = 8
-victory_modulo: int = 100
-
-
-class StarcraftClientProcessor(ClientCommandProcessor):
- ctx: SC2Context
-
- def _cmd_difficulty(self, difficulty: str = "") -> bool:
- """Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal"""
- options = difficulty.split()
- num_options = len(options)
-
- if num_options > 0:
- difficulty_choice = options[0].lower()
- if difficulty_choice == "casual":
- self.ctx.difficulty_override = 0
- elif difficulty_choice == "normal":
- self.ctx.difficulty_override = 1
- elif difficulty_choice == "hard":
- self.ctx.difficulty_override = 2
- elif difficulty_choice == "brutal":
- self.ctx.difficulty_override = 3
- else:
- self.output("Unable to parse difficulty '" + options[0] + "'")
- return False
-
- self.output("Difficulty set to " + options[0])
- return True
-
- else:
- if self.ctx.difficulty == -1:
- self.output("Please connect to a seed before checking difficulty.")
- else:
- self.output("Current difficulty: " + ["Casual", "Normal", "Hard", "Brutal"][self.ctx.difficulty])
- self.output("To change the difficulty, add the name of the difficulty after the command.")
- return False
-
- def _cmd_disable_mission_check(self) -> bool:
- """Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play
- the next mission in a chain the other player is doing."""
- self.ctx.missions_unlocked = True
- sc2_logger.info("Mission check has been disabled")
- return True
-
- def _cmd_play(self, mission_id: str = "") -> bool:
- """Start a Starcraft 2 mission"""
-
- options = mission_id.split()
- num_options = len(options)
-
- if num_options > 0:
- mission_number = int(options[0])
-
- self.ctx.play_mission(mission_number)
-
- else:
- sc2_logger.info(
- "Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.")
- return False
-
- return True
-
- def _cmd_available(self) -> bool:
- """Get what missions are currently available to play"""
-
- request_available_missions(self.ctx)
- return True
-
- def _cmd_unfinished(self) -> bool:
- """Get what missions are currently available to play and have not had all locations checked"""
-
- request_unfinished_missions(self.ctx)
- return True
-
- @mark_raw
- def _cmd_set_path(self, path: str = '') -> bool:
- """Manually set the SC2 install directory (if the automatic detection fails)."""
- if path:
- os.environ["SC2PATH"] = path
- is_mod_installed_correctly()
- return True
- else:
- sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.")
- return False
-
- def _cmd_download_data(self) -> bool:
- """Download the most recent release of the necessary files for playing SC2 with
- Archipelago. Will overwrite existing files."""
- if "SC2PATH" not in os.environ:
- check_game_install_path()
-
- if os.path.exists(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt"):
- with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "r") as f:
- current_ver = f.read()
- else:
- current_ver = None
-
- tempzip, version = download_latest_release_zip('TheCondor07', 'Starcraft2ArchipelagoData',
- current_version=current_ver, force_download=True)
-
- if tempzip != '':
- try:
- zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"])
- sc2_logger.info(f"Download complete. Version {version} installed.")
- with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "w") as f:
- f.write(version)
- finally:
- os.remove(tempzip)
- else:
- sc2_logger.warning("Download aborted/failed. Read the log for more information.")
- return False
- return True
-
-
-class SC2Context(CommonContext):
- command_processor = StarcraftClientProcessor
- game = "Starcraft 2 Wings of Liberty"
- items_handling = 0b111
- difficulty = -1
- all_in_choice = 0
- mission_order = 0
- mission_req_table: typing.Dict[str, MissionInfo] = {}
- final_mission: int = 29
- announcements = queue.Queue()
- sc2_run_task: typing.Optional[asyncio.Task] = None
- missions_unlocked: bool = False # allow launching missions ignoring requirements
- current_tooltip = None
- last_loc_list = None
- difficulty_override = -1
- mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {}
- last_bot: typing.Optional[ArchipelagoBot] = None
-
- def __init__(self, *args, **kwargs):
- super(SC2Context, self).__init__(*args, **kwargs)
- self.raw_text_parser = RawJSONtoTextParser(self)
-
- async def server_auth(self, password_requested: bool = False):
- if password_requested and not self.password:
- await super(SC2Context, self).server_auth(password_requested)
- await self.get_username()
- await self.send_connect()
-
- def on_package(self, cmd: str, args: dict):
- if cmd in {"Connected"}:
- self.difficulty = args["slot_data"]["game_difficulty"]
- self.all_in_choice = args["slot_data"]["all_in_map"]
- slot_req_table = args["slot_data"]["mission_req"]
- # Maintaining backwards compatibility with older slot data
- self.mission_req_table = {
- mission: MissionInfo(
- **{field: value for field, value in mission_info.items() if field in MissionInfo._fields}
- )
- for mission, mission_info in slot_req_table.items()
- }
- self.mission_order = args["slot_data"].get("mission_order", 0)
- self.final_mission = args["slot_data"].get("final_mission", 29)
-
- self.build_location_to_mission_mapping()
-
- # Looks for the required maps and mods for SC2. Runs check_game_install_path.
- maps_present = is_mod_installed_correctly()
- if os.path.exists(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt"):
- with open(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt", "r") as f:
- current_ver = f.read()
- if is_mod_update_available("TheCondor07", "Starcraft2ArchipelagoData", current_ver):
- sc2_logger.info("NOTICE: Update for required files found. Run /download_data to install.")
- elif maps_present:
- sc2_logger.warning("NOTICE: Your map files may be outdated (version number not found). "
- "Run /download_data to update them.")
-
-
- def on_print_json(self, args: dict):
- # goes to this world
- if "receiving" in args and self.slot_concerns_self(args["receiving"]):
- relevant = True
- # found in this world
- elif "item" in args and self.slot_concerns_self(args["item"].player):
- relevant = True
- # not related
- else:
- relevant = False
-
- if relevant:
- self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"])))
-
- super(SC2Context, self).on_print_json(args)
-
- def run_gui(self):
- from kvui import GameManager, HoverBehavior, ServerToolTip
- from kivy.app import App
- from kivy.clock import Clock
- from kivy.uix.tabbedpanel import TabbedPanelItem
- from kivy.uix.gridlayout import GridLayout
- from kivy.lang import Builder
- from kivy.uix.label import Label
- from kivy.uix.button import Button
- from kivy.uix.floatlayout import FloatLayout
- from kivy.properties import StringProperty
-
- class HoverableButton(HoverBehavior, Button):
- pass
-
- class MissionButton(HoverableButton):
- tooltip_text = StringProperty("Test")
- ctx: SC2Context
-
- def __init__(self, *args, **kwargs):
- super(HoverableButton, self).__init__(*args, **kwargs)
- self.layout = FloatLayout()
- self.popuplabel = ServerToolTip(text=self.text)
- self.layout.add_widget(self.popuplabel)
-
- def on_enter(self):
- self.popuplabel.text = self.tooltip_text
-
- if self.ctx.current_tooltip:
- App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
-
- if self.tooltip_text == "":
- self.ctx.current_tooltip = None
- else:
- App.get_running_app().root.add_widget(self.layout)
- self.ctx.current_tooltip = self.layout
-
- def on_leave(self):
- self.ctx.ui.clear_tooltip()
-
- @property
- def ctx(self) -> CommonContext:
- return App.get_running_app().ctx
-
- class MissionLayout(GridLayout):
- pass
-
- class MissionCategory(GridLayout):
- pass
-
- class SC2Manager(GameManager):
- logging_pairs = [
- ("Client", "Archipelago"),
- ("Starcraft2", "Starcraft2"),
- ]
- base_title = "Archipelago Starcraft 2 Client"
-
- mission_panel = None
- last_checked_locations = {}
- mission_id_to_button = {}
- launching: typing.Union[bool, int] = False # if int -> mission ID
- refresh_from_launching = True
- first_check = True
- ctx: SC2Context
-
- def __init__(self, ctx):
- super().__init__(ctx)
-
- def clear_tooltip(self):
- if self.ctx.current_tooltip:
- App.get_running_app().root.remove_widget(self.ctx.current_tooltip)
-
- self.ctx.current_tooltip = None
-
- def build(self):
- container = super().build()
-
- panel = TabbedPanelItem(text="Starcraft 2 Launcher")
- self.mission_panel = panel.content = MissionLayout()
-
- self.tabs.add_widget(panel)
-
- Clock.schedule_interval(self.build_mission_table, 0.5)
-
- return container
-
- def build_mission_table(self, dt):
- if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or
- not self.refresh_from_launching)) or self.first_check:
- self.refresh_from_launching = True
-
- self.mission_panel.clear_widgets()
- if self.ctx.mission_req_table:
- self.last_checked_locations = self.ctx.checked_locations.copy()
- self.first_check = False
-
- self.mission_id_to_button = {}
- categories = {}
- available_missions, unfinished_missions = calc_unfinished_missions(self.ctx)
-
- # separate missions into categories
- for mission in self.ctx.mission_req_table:
- if not self.ctx.mission_req_table[mission].category in categories:
- categories[self.ctx.mission_req_table[mission].category] = []
-
- categories[self.ctx.mission_req_table[mission].category].append(mission)
-
- for category in categories:
- category_panel = MissionCategory()
- if category.startswith('_'):
- category_display_name = ''
- else:
- category_display_name = category
- category_panel.add_widget(
- Label(text=category_display_name, size_hint_y=None, height=50, outline_width=1))
-
- for mission in categories[category]:
- text: str = mission
- tooltip: str = ""
- mission_id: int = self.ctx.mission_req_table[mission].id
- # Map has uncollected locations
- if mission in unfinished_missions:
- text = f"[color=6495ED]{text}[/color]"
- elif mission in available_missions:
- text = f"[color=FFFFFF]{text}[/color]"
- # Map requirements not met
- else:
- text = f"[color=a9a9a9]{text}[/color]"
- tooltip = f"Requires: "
- if self.ctx.mission_req_table[mission].required_world:
- tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for
- req_mission in
- self.ctx.mission_req_table[mission].required_world)
-
- if self.ctx.mission_req_table[mission].number:
- tooltip += " and "
- if self.ctx.mission_req_table[mission].number:
- tooltip += f"{self.ctx.mission_req_table[mission].number} missions completed"
- remaining_location_names: typing.List[str] = [
- self.ctx.location_names[loc] for loc in self.ctx.locations_for_mission(mission)
- if loc in self.ctx.missing_locations]
-
- if mission_id == self.ctx.final_mission:
- if mission in available_missions:
- text = f"[color=FFBC95]{mission}[/color]"
- else:
- text = f"[color=D0C0BE]{mission}[/color]"
- if tooltip:
- tooltip += "\n"
- tooltip += "Final Mission"
-
- if remaining_location_names:
- if tooltip:
- tooltip += "\n"
- tooltip += f"Uncollected locations:\n"
- tooltip += "\n".join(remaining_location_names)
-
- mission_button = MissionButton(text=text, size_hint_y=None, height=50)
- mission_button.tooltip_text = tooltip
- mission_button.bind(on_press=self.mission_callback)
- self.mission_id_to_button[mission_id] = mission_button
- category_panel.add_widget(mission_button)
-
- category_panel.add_widget(Label(text=""))
- self.mission_panel.add_widget(category_panel)
-
- elif self.launching:
- self.refresh_from_launching = False
-
- self.mission_panel.clear_widgets()
- self.mission_panel.add_widget(Label(text="Launching Mission: " +
- lookup_id_to_mission[self.launching]))
- if self.ctx.ui:
- self.ctx.ui.clear_tooltip()
-
- def mission_callback(self, button):
- if not self.launching:
- mission_id: int = next(k for k, v in self.mission_id_to_button.items() if v == button)
- self.ctx.play_mission(mission_id)
- self.launching = mission_id
- Clock.schedule_once(self.finish_launching, 10)
-
- def finish_launching(self, dt):
- self.launching = False
-
- self.ui = SC2Manager(self)
- self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
- import pkgutil
- data = pkgutil.get_data(SC2WoLWorld.__module__, "Starcraft2.kv").decode()
- Builder.load_string(data)
-
- async def shutdown(self):
- await super(SC2Context, self).shutdown()
- if self.last_bot:
- self.last_bot.want_close = True
- if self.sc2_run_task:
- self.sc2_run_task.cancel()
-
- def play_mission(self, mission_id: int):
- if self.missions_unlocked or \
- is_mission_available(self, mission_id):
- if self.sc2_run_task:
- if not self.sc2_run_task.done():
- sc2_logger.warning("Starcraft 2 Client is still running!")
- self.sc2_run_task.cancel() # doesn't actually close the game, just stops the python task
- if self.slot is None:
- sc2_logger.warning("Launching Mission without Archipelago authentication, "
- "checks will not be registered to server.")
- self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id),
- name="Starcraft 2 Launch")
- else:
- sc2_logger.info(
- f"{lookup_id_to_mission[mission_id]} is not currently unlocked. "
- f"Use /unfinished or /available to see what is available.")
-
- def build_location_to_mission_mapping(self):
- mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = {
- mission_info.id: set() for mission_info in self.mission_req_table.values()
- }
-
- for loc in self.server_locations:
- mission_id, objective = divmod(loc - SC2WOL_LOC_ID_OFFSET, victory_modulo)
- mission_id_to_location_ids[mission_id].add(objective)
- self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in
- mission_id_to_location_ids.items()}
-
- def locations_for_mission(self, mission: str):
- mission_id: int = self.mission_req_table[mission].id
- objectives = self.mission_id_to_location_ids[self.mission_req_table[mission].id]
- for objective in objectives:
- yield SC2WOL_LOC_ID_OFFSET + mission_id * 100 + objective
-
-
-async def main():
- multiprocessing.freeze_support()
- parser = get_base_parser()
- parser.add_argument('--name', default=None, help="Slot Name to connect as.")
- args = parser.parse_args()
-
- ctx = SC2Context(args.connect, args.password)
- ctx.auth = args.name
- if ctx.server_task is None:
- ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
-
- if gui_enabled:
- ctx.run_gui()
- ctx.run_cli()
-
- await ctx.exit_event.wait()
-
- await ctx.shutdown()
-
-
-maps_table = [
- "ap_traynor01", "ap_traynor02", "ap_traynor03",
- "ap_thanson01", "ap_thanson02", "ap_thanson03a", "ap_thanson03b",
- "ap_ttychus01", "ap_ttychus02", "ap_ttychus03", "ap_ttychus04", "ap_ttychus05",
- "ap_ttosh01", "ap_ttosh02", "ap_ttosh03a", "ap_ttosh03b",
- "ap_thorner01", "ap_thorner02", "ap_thorner03", "ap_thorner04", "ap_thorner05s",
- "ap_tzeratul01", "ap_tzeratul02", "ap_tzeratul03", "ap_tzeratul04",
- "ap_tvalerian01", "ap_tvalerian02a", "ap_tvalerian02b", "ap_tvalerian03"
-]
-
-wol_default_categories = [
- "Mar Sara", "Mar Sara", "Mar Sara", "Colonist", "Colonist", "Colonist", "Colonist",
- "Artifact", "Artifact", "Artifact", "Artifact", "Artifact", "Covert", "Covert", "Covert", "Covert",
- "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Prophecy", "Prophecy", "Prophecy", "Prophecy",
- "Char", "Char", "Char", "Char"
-]
-wol_default_category_names = [
- "Mar Sara", "Colonist", "Artifact", "Covert", "Rebellion", "Prophecy", "Char"
-]
-
-
-def calculate_items(items: typing.List[NetworkItem]) -> typing.List[int]:
- network_item: NetworkItem
- accumulators: typing.List[int] = [0 for _ in type_flaggroups]
-
- for network_item in items:
- name: str = lookup_id_to_name[network_item.item]
- item_data: ItemData = item_table[name]
-
- # exists exactly once
- if item_data.quantity == 1:
- accumulators[type_flaggroups[item_data.type]] |= 1 << item_data.number
-
- # exists multiple times
- elif item_data.type == "Upgrade":
- accumulators[type_flaggroups[item_data.type]] += 1 << item_data.number
-
- # sum
- else:
- accumulators[type_flaggroups[item_data.type]] += item_data.number
-
- return accumulators
-
-
-def calc_difficulty(difficulty):
- if difficulty == 0:
- return 'C'
- elif difficulty == 1:
- return 'N'
- elif difficulty == 2:
- return 'H'
- elif difficulty == 3:
- return 'B'
-
- return 'X'
-
-
-async def starcraft_launch(ctx: SC2Context, mission_id: int):
- sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.")
-
- with DllDirectory(None):
- run_game(bot.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
- name="Archipelago", fullscreen=True)], realtime=True)
-
-
-class ArchipelagoBot(bot.bot_ai.BotAI):
- game_running: bool = False
- mission_completed: bool = False
- boni: typing.List[bool]
- setup_done: bool
- ctx: SC2Context
- mission_id: int
- want_close: bool = False
- can_read_game = False
-
- last_received_update: int = 0
-
- def __init__(self, ctx: SC2Context, mission_id):
- self.setup_done = False
- self.ctx = ctx
- self.ctx.last_bot = self
- self.mission_id = mission_id
- self.boni = [False for _ in range(max_bonus)]
-
- super(ArchipelagoBot, self).__init__()
-
- async def on_step(self, iteration: int):
- if self.want_close:
- self.want_close = False
- await self._client.leave()
- return
- game_state = 0
- if not self.setup_done:
- self.setup_done = True
- start_items = calculate_items(self.ctx.items_received)
- if self.ctx.difficulty_override >= 0:
- difficulty = calc_difficulty(self.ctx.difficulty_override)
- else:
- difficulty = calc_difficulty(self.ctx.difficulty)
- await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {} {}".format(
- difficulty,
- start_items[0], start_items[1], start_items[2], start_items[3], start_items[4],
- start_items[5], start_items[6], start_items[7], start_items[8], start_items[9],
- self.ctx.all_in_choice, start_items[10]))
- self.last_received_update = len(self.ctx.items_received)
-
- else:
- if not self.ctx.announcements.empty():
- message = self.ctx.announcements.get(timeout=1)
- await self.chat_send("SendMessage " + message)
- self.ctx.announcements.task_done()
-
- # Archipelago reads the health
- for unit in self.all_own_units():
- if unit.health_max == 38281:
- game_state = int(38281 - unit.health)
- self.can_read_game = True
-
- if iteration == 160 and not game_state & 1:
- await self.chat_send("SendMessage Warning: Archipelago unable to connect or has lost connection to " +
- "Starcraft 2 (This is likely a map issue)")
-
- if self.last_received_update < len(self.ctx.items_received):
- current_items = calculate_items(self.ctx.items_received)
- await self.chat_send("UpdateTech {} {} {} {} {} {} {} {}".format(
- current_items[0], current_items[1], current_items[2], current_items[3], current_items[4],
- current_items[5], current_items[6], current_items[7]))
- self.last_received_update = len(self.ctx.items_received)
-
- if game_state & 1:
- if not self.game_running:
- print("Archipelago Connected")
- self.game_running = True
-
- if self.can_read_game:
- if game_state & (1 << 1) and not self.mission_completed:
- if self.mission_id != self.ctx.final_mission:
- print("Mission Completed")
- await self.ctx.send_msgs(
- [{"cmd": 'LocationChecks',
- "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id]}])
- self.mission_completed = True
- else:
- print("Game Complete")
- await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}])
- self.mission_completed = True
-
- for x, completed in enumerate(self.boni):
- if not completed and game_state & (1 << (x + 2)):
- await self.ctx.send_msgs(
- [{"cmd": 'LocationChecks',
- "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id + x + 1]}])
- self.boni[x] = True
-
- else:
- await self.chat_send("LostConnection - Lost connection to game.")
-
-
-def request_unfinished_missions(ctx: SC2Context):
- if ctx.mission_req_table:
- message = "Unfinished Missions: "
- unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
- unfinished_locations = initialize_blank_mission_dict(ctx.mission_req_table)
-
- _, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks)
-
- # Removing All-In from location pool
- final_mission = lookup_id_to_mission[ctx.final_mission]
- if final_mission in unfinished_missions.keys():
- message = f"Final Mission Available: {final_mission}[{ctx.final_mission}]\n" + message
- if unfinished_missions[final_mission] == -1:
- unfinished_missions.pop(final_mission)
-
- message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[mission].id}] " +
- mark_up_objectives(
- f"[{len(unfinished_missions[mission])}/"
- f"{sum(1 for _ in ctx.locations_for_mission(mission))}]",
- ctx, unfinished_locations, mission)
- for mission in unfinished_missions)
-
- if ctx.ui:
- ctx.ui.log_panels['All'].on_message_markup(message)
- ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
- else:
- sc2_logger.info(message)
- else:
- sc2_logger.warning("No mission table found, you are likely not connected to a server.")
-
-
-def calc_unfinished_missions(ctx: SC2Context, unlocks=None):
- unfinished_missions = []
- locations_completed = []
-
- if not unlocks:
- unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
-
- available_missions = calc_available_missions(ctx, unlocks)
-
- for name in available_missions:
- objectives = set(ctx.locations_for_mission(name))
- if objectives:
- objectives_completed = ctx.checked_locations & objectives
- if len(objectives_completed) < len(objectives):
- unfinished_missions.append(name)
- locations_completed.append(objectives_completed)
-
- else: # infer that this is the final mission as it has no objectives
- unfinished_missions.append(name)
- locations_completed.append(-1)
-
- return available_missions, dict(zip(unfinished_missions, locations_completed))
-
-
-def is_mission_available(ctx: SC2Context, mission_id_to_check):
- unfinished_missions = calc_available_missions(ctx)
-
- return any(mission_id_to_check == ctx.mission_req_table[mission].id for mission in unfinished_missions)
-
-
-def mark_up_mission_name(ctx: SC2Context, mission, unlock_table):
- """Checks if the mission is required for game completion and adds '*' to the name to mark that."""
-
- if ctx.mission_req_table[mission].completion_critical:
- if ctx.ui:
- message = "[color=AF99EF]" + mission + "[/color]"
- else:
- message = "*" + mission + "*"
- else:
- message = mission
-
- if ctx.ui:
- unlocks = unlock_table[mission]
-
- if len(unlocks) > 0:
- pre_message = f"[ref={list(ctx.mission_req_table).index(mission)}|Unlocks: "
- pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[unlock].id})" for unlock in unlocks)
- pre_message += f"]"
- message = pre_message + message + "[/ref]"
-
- return message
-
-
-def mark_up_objectives(message, ctx, unfinished_locations, mission):
- formatted_message = message
-
- if ctx.ui:
- locations = unfinished_locations[mission]
-
- pre_message = f"[ref={list(ctx.mission_req_table).index(mission) + 30}|"
- pre_message += " ".join(location for location in locations)
- pre_message += f"]"
- formatted_message = pre_message + message + "[/ref]"
-
- return formatted_message
-
-
-def request_available_missions(ctx: SC2Context):
- if ctx.mission_req_table:
- message = "Available Missions: "
-
- # Initialize mission unlock table
- unlocks = initialize_blank_mission_dict(ctx.mission_req_table)
-
- missions = calc_available_missions(ctx, unlocks)
- message += \
- ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}"
- f"[{ctx.mission_req_table[mission].id}]"
- for mission in missions)
-
- if ctx.ui:
- ctx.ui.log_panels['All'].on_message_markup(message)
- ctx.ui.log_panels['Starcraft2'].on_message_markup(message)
- else:
- sc2_logger.info(message)
- else:
- sc2_logger.warning("No mission table found, you are likely not connected to a server.")
-
-
-def calc_available_missions(ctx: SC2Context, unlocks=None):
- available_missions = []
- missions_complete = 0
-
- # Get number of missions completed
- for loc in ctx.checked_locations:
- if loc % victory_modulo == 0:
- missions_complete += 1
-
- for name in ctx.mission_req_table:
- # Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips
- if unlocks:
- for unlock in ctx.mission_req_table[name].required_world:
- unlocks[list(ctx.mission_req_table)[unlock - 1]].append(name)
-
- if mission_reqs_completed(ctx, name, missions_complete):
- available_missions.append(name)
-
- return available_missions
-
-
-def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete: int):
- """Returns a bool signifying if the mission has all requirements complete and can be done
-
- Arguments:
- ctx -- instance of SC2Context
- locations_to_check -- the mission string name to check
- missions_complete -- an int of how many missions have been completed
- mission_path -- a list of missions that have already been checked
-"""
- if len(ctx.mission_req_table[mission_name].required_world) >= 1:
- # A check for when the requirements are being or'd
- or_success = False
-
- # Loop through required missions
- for req_mission in ctx.mission_req_table[mission_name].required_world:
- req_success = True
-
- # Check if required mission has been completed
- if not (ctx.mission_req_table[list(ctx.mission_req_table)[req_mission - 1]].id *
- victory_modulo + SC2WOL_LOC_ID_OFFSET) in ctx.checked_locations:
- if not ctx.mission_req_table[mission_name].or_requirements:
- return False
- else:
- req_success = False
-
- # Grid-specific logic (to avoid long path checks and infinite recursion)
- if ctx.mission_order in (3, 4):
- if req_success:
- return True
- else:
- if req_mission is ctx.mission_req_table[mission_name].required_world[-1]:
- return False
- else:
- continue
-
- # Recursively check required mission to see if it's requirements are met, in case !collect has been done
- # Skipping recursive check on Grid settings to speed up checks and avoid infinite recursion
- if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete):
- if not ctx.mission_req_table[mission_name].or_requirements:
- return False
- else:
- req_success = False
-
- # If requirement check succeeded mark or as satisfied
- if ctx.mission_req_table[mission_name].or_requirements and req_success:
- or_success = True
-
- if ctx.mission_req_table[mission_name].or_requirements:
- # Return false if or requirements not met
- if not or_success:
- return False
-
- # Check number of missions
- if missions_complete >= ctx.mission_req_table[mission_name].number:
- return True
- else:
- return False
- else:
- return True
-
-
-def initialize_blank_mission_dict(location_table):
- unlocks = {}
-
- for mission in list(location_table):
- unlocks[mission] = []
-
- return unlocks
-
-
-def check_game_install_path() -> bool:
- # First thing: go to the default location for ExecuteInfo.
- # An exception for Windows is included because it's very difficult to find ~\Documents if the user moved it.
- if is_windows:
- # The next five lines of utterly inscrutable code are brought to you by copy-paste from Stack Overflow.
- # https://stackoverflow.com/questions/6227590/finding-the-users-my-documents-path/30924555#
- import ctypes.wintypes
- CSIDL_PERSONAL = 5 # My Documents
- SHGFP_TYPE_CURRENT = 0 # Get current, not default value
-
- buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
- ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf)
- documentspath = buf.value
- einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt"))
- else:
- einfo = str(bot.paths.get_home() / Path(bot.paths.USERPATH[bot.paths.PF]))
-
- # Check if the file exists.
- if os.path.isfile(einfo):
-
- # Open the file and read it, picking out the latest executable's path.
- with open(einfo) as f:
- content = f.read()
- if content:
- try:
- base = re.search(r" = (.*)Versions", content).group(1)
- except AttributeError:
- sc2_logger.warning(f"Found {einfo}, but it was empty. Run SC2 through the Blizzard launcher, then "
- f"try again.")
- return False
- if os.path.exists(base):
- executable = bot.paths.latest_executeble(Path(base).expanduser() / "Versions")
-
- # Finally, check the path for an actual executable.
- # If we find one, great. Set up the SC2PATH.
- if os.path.isfile(executable):
- sc2_logger.info(f"Found an SC2 install at {base}!")
- sc2_logger.debug(f"Latest executable at {executable}.")
- os.environ["SC2PATH"] = base
- sc2_logger.debug(f"SC2PATH set to {base}.")
- return True
- else:
- sc2_logger.warning(f"We may have found an SC2 install at {base}, but couldn't find {executable}.")
- else:
- sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.")
- else:
- sc2_logger.warning(f"Couldn't find {einfo}. Run SC2 through the Blizzard launcher, then try again. "
- f"If that fails, please run /set_path with your SC2 install directory.")
- return False
-
-
-def is_mod_installed_correctly() -> bool:
- """Searches for all required files."""
- if "SC2PATH" not in os.environ:
- check_game_install_path()
-
- mapdir = os.environ['SC2PATH'] / Path('Maps/ArchipelagoCampaign')
- modfile = os.environ["SC2PATH"] / Path("Mods/Archipelago.SC2Mod")
- wol_required_maps = [
- "ap_thanson01.SC2Map", "ap_thanson02.SC2Map", "ap_thanson03a.SC2Map", "ap_thanson03b.SC2Map",
- "ap_thorner01.SC2Map", "ap_thorner02.SC2Map", "ap_thorner03.SC2Map", "ap_thorner04.SC2Map", "ap_thorner05s.SC2Map",
- "ap_traynor01.SC2Map", "ap_traynor02.SC2Map", "ap_traynor03.SC2Map",
- "ap_ttosh01.SC2Map", "ap_ttosh02.SC2Map", "ap_ttosh03a.SC2Map", "ap_ttosh03b.SC2Map",
- "ap_ttychus01.SC2Map", "ap_ttychus02.SC2Map", "ap_ttychus03.SC2Map", "ap_ttychus04.SC2Map", "ap_ttychus05.SC2Map",
- "ap_tvalerian01.SC2Map", "ap_tvalerian02a.SC2Map", "ap_tvalerian02b.SC2Map", "ap_tvalerian03.SC2Map",
- "ap_tzeratul01.SC2Map", "ap_tzeratul02.SC2Map", "ap_tzeratul03.SC2Map", "ap_tzeratul04.SC2Map"
- ]
- needs_files = False
-
- # Check for maps.
- missing_maps = []
- for mapfile in wol_required_maps:
- if not os.path.isfile(mapdir / mapfile):
- missing_maps.append(mapfile)
- if len(missing_maps) >= 19:
- sc2_logger.warning(f"All map files missing from {mapdir}.")
- needs_files = True
- elif len(missing_maps) > 0:
- for map in missing_maps:
- sc2_logger.debug(f"Missing {map} from {mapdir}.")
- sc2_logger.warning(f"Missing {len(missing_maps)} map files.")
- needs_files = True
- else: # Must be no maps missing
- sc2_logger.info(f"All maps found in {mapdir}.")
-
- # Check for mods.
- if os.path.isfile(modfile):
- sc2_logger.info(f"Archipelago mod found at {modfile}.")
- else:
- sc2_logger.warning(f"Archipelago mod could not be found at {modfile}.")
- needs_files = True
-
- # Final verdict.
- if needs_files:
- sc2_logger.warning(f"Required files are missing. Run /download_data to acquire them.")
- return False
- else:
- return True
-
-
-class DllDirectory:
- # Credit to Black Sliver for this code.
- # More info: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw
- _old: typing.Optional[str] = None
- _new: typing.Optional[str] = None
-
- def __init__(self, new: typing.Optional[str]):
- self._new = new
-
- def __enter__(self):
- old = self.get()
- if self.set(self._new):
- self._old = old
-
- def __exit__(self, *args):
- if self._old is not None:
- self.set(self._old)
-
- @staticmethod
- def get() -> typing.Optional[str]:
- if sys.platform == "win32":
- n = ctypes.windll.kernel32.GetDllDirectoryW(0, None)
- buf = ctypes.create_unicode_buffer(n)
- ctypes.windll.kernel32.GetDllDirectoryW(n, buf)
- return buf.value
- # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
- return None
-
- @staticmethod
- def set(s: typing.Optional[str]) -> bool:
- if sys.platform == "win32":
- return ctypes.windll.kernel32.SetDllDirectoryW(s) != 0
- # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
- return False
-
-
-def download_latest_release_zip(owner: str, repo: str, current_version: str = None, force_download=False) -> (str, str):
- """Downloads the latest release of a GitHub repo to the current directory as a .zip file."""
- import requests
-
- headers = {"Accept": 'application/vnd.github.v3+json'}
- url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
-
- r1 = requests.get(url, headers=headers)
- if r1.status_code == 200:
- latest_version = r1.json()["tag_name"]
- sc2_logger.info(f"Latest version: {latest_version}.")
- else:
- sc2_logger.warning(f"Status code: {r1.status_code}")
- sc2_logger.warning(f"Failed to reach GitHub. Could not find download link.")
- sc2_logger.warning(f"text: {r1.text}")
- return "", current_version
-
- if (force_download is False) and (current_version == latest_version):
- sc2_logger.info("Latest version already installed.")
- return "", current_version
-
- sc2_logger.info(f"Attempting to download version {latest_version} of {repo}.")
- download_url = r1.json()["assets"][0]["browser_download_url"]
-
- r2 = requests.get(download_url, headers=headers)
- if r2.status_code == 200 and zipfile.is_zipfile(io.BytesIO(r2.content)):
- with open(f"{repo}.zip", "wb") as fh:
- fh.write(r2.content)
- sc2_logger.info(f"Successfully downloaded {repo}.zip.")
- return f"{repo}.zip", latest_version
- else:
- sc2_logger.warning(f"Status code: {r2.status_code}")
- sc2_logger.warning("Download failed.")
- sc2_logger.warning(f"text: {r2.text}")
- return "", current_version
-
-
-def is_mod_update_available(owner: str, repo: str, current_version: str) -> bool:
- import requests
-
- headers = {"Accept": 'application/vnd.github.v3+json'}
- url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
-
- r1 = requests.get(url, headers=headers)
- if r1.status_code == 200:
- latest_version = r1.json()["tag_name"]
- if current_version != latest_version:
- return True
- else:
- return False
-
- else:
- sc2_logger.warning(f"Failed to reach GitHub while checking for updates.")
- sc2_logger.warning(f"Status code: {r1.status_code}")
- sc2_logger.warning(f"text: {r1.text}")
- return False
-
-
-if __name__ == '__main__':
- colorama.init()
- asyncio.run(main())
- colorama.deinit()
+ Utils.init_logging("Starcraft2Client", exception_logger="Client")
+ launch()
diff --git a/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png b/WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png
new file mode 100644
index 0000000000000000000000000000000000000000..8fb366b93ff0debd825faf213132da839c04a89b
GIT binary patch
literal 5945
zcmV-97slv`P)EX>4Tx04R}tkv&MmKpe$iTct%Rg6&YmAwzYtAS&XhRVYG*P%E_RU~=gfG-*gu
zTpR`0f`cE6RRF*B8G^>Onpui)9@T$_we!cF2S?B&;2?2m4e9tpFljzbi*RvAfDc|
zbk6(4QC5}|;&b9LgDyz?$aUG}H_j!8{X8>jWHa-`QDULg#c~(3vY`@B5yur(qkMnP
zWrgz=XSG^q?R)YUh6~!tGS_JiBZWmQL4*JqbyQG=g#@h{DJC+spY-q#Iew8`GPx>X
z
z&wb2$Mx%!%S&}2gj-4h>+}iGoKtrKW3WY*x8wf3wv?TvR|AYP!rG>UBEtC={q2y5#
zCuvCH#7QGtk}X-1WqC##&D^;&_j%4){b7IiIeV@~g5F_tukM`3-fMl=THp0JF8dp-
z`Z|z;4S*rw^lLyDm;x=Jr|%MQPUq|chd}PXXXejwi@+&xYd~ASPS>gQxzzO>{#5`Y
ze?1!jd*IFkCw1H|*iBvcS+MuPehy?bFRcrwdnW#~%lx`*0O=nicDIm%*I|y8vzgZ1}(1p^&!#ZUL${rKmDmYpH9rBV+5p6d9Y|*At{Z`;Q{@p5IMR2d04Z5~WCLkzWx6-jrT3QFzy|7&A
z|7Le06B_xA(E@uLY!V?RTw^)X{8Z-#lokHhrjWUwU>g>b=)^LzK_WN?$j+iotV`Ri
zMx$;+$1ebDIPgq%d1SkU)-9_r+}cOPu*g=qJ5OyncnGH^oIn?*b^YFAoT_}?+IN6#-S_v1-{6$^r?b&8?LQ1r;L^o=I%fS-=P97mAz~4n?m4cb<
z`VBd|M_Sj(XiZv|+w?vqY^pY;V48hhyQ_6N$XEbYrL9q<*+ibOj1bX_99-IL&=CnG
z_65#w)NO)0EBjxSKN)L*E4t?hY^O0P*9R-*yoUPiYK^T1-fkT@3vSg5%Bsj@o$eJJ
zUC_FNjAYK?97dW*B!B?vWkGRW(og*2!r!g%pY6~u)cp#$=fS<~oz_aaLQc*y%k)@a
z!i0*7l7jo(XMAt$VeAynI*|T}zn5Ku>N|q?QiD345zGV9N+~A+8%8L063p29kBki5
z3Rp`}nd+d9Bo(xnOKaX(qb2FY*3AMu{Jzy!ga>F
zJaD+dy`#>-WpmERBDHqxo9K~I2_74fbC^Z8G1Y}Sa%3H-Zh^fC
z_TF5)pLz|u&o`mZ;aFgigp`yNr=KQ5W)u`8BrLPS3akE-TTuNF+&OR`mm~W$a7#+)
zfcp&C_oW=k(t-mM)NSNLBg4qynZg;H(s~K*8Q^}z%LANJIgu`~4fat)K&FkI+57(H
z?v%etn>GvdXp_^TMMZ^S_105TIk=LFiWyUX-GN7-U4`WH{vPfU@CmRFEnC3UoR%hk>DQF}j@LYEQc^(chg#=M=GGD2T?O|dWYu@U
z{zrDS1?d<4AM71%YDLaXjslBp>QG)Y_m0se(n42)n0f22s3<6C)d3hPam}Po4ourm
zJc6cCNJXJE@ks_3#JTSQ&q4B=z)Mj5)Ci#Pzh1=U;L
z8(Uwtvl2O}v8377_icUGRfrq-1jD6*`Z`?X5*<4ACWDg;?|!QZ6$uFmODxe{?=bxA
z826&C`Dwoix8WCW+d}C#z`X#;@A~Y^T~Peq2Y1joGxs96i`skwc0UjpY0^ee{Tn2o
zgXB}vdg^cJE{d46L{cnrdJ}1=BY0WEnO&q^jsV?7QOK)x$PmM5(`EG97*|5O+oq(X
zC=1dwrFg5vehkTPfcqGfUxV`R!Tp+0dlJ$=0=pGCG}RSs52|kmuI?hQT=>nEe+%iC
zJyBpm=k}E={|Ks|L>W-9^O=Nq3#y-idkD$zLyMe5KK;0t$rw^ndi1!!MPB7KM){B_
zQ})?spFWS$M~sFe*n{Nvyr$WM^bh@ds(+B8*1SMiYRtv1$x$9Efx4n_Rwwac@zZS1E})LBv5yy#Kv&iXwy8m}5QW&l2RI9;2h^^k