Skip to content

Commit eb30a71

Browse files
committed
Merge branch 'main' into smw-main
2 parents f33d98b + dc2aa5f commit eb30a71

37 files changed

+309
-148
lines changed

BaseClasses.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -393,15 +393,25 @@ def get_all_state(self, use_cache: bool) -> CollectionState:
393393
def get_items(self) -> List[Item]:
394394
return [loc.item for loc in self.get_filled_locations()] + self.itempool
395395

396-
def find_item_locations(self, item, player: int) -> List[Location]:
396+
def find_item_locations(self, item, player: int, resolve_group_locations: bool = False) -> List[Location]:
397+
if resolve_group_locations:
398+
player_groups = self.get_player_groups(player)
399+
return [location for location in self.get_locations() if
400+
location.item and location.item.name == item and location.player not in player_groups and
401+
(location.item.player == player or location.item.player in player_groups)]
397402
return [location for location in self.get_locations() if
398403
location.item and location.item.name == item and location.item.player == player]
399404

400405
def find_item(self, item, player: int) -> Location:
401406
return next(location for location in self.get_locations() if
402407
location.item and location.item.name == item and location.item.player == player)
403408

404-
def find_items_in_locations(self, items: Set[str], player: int) -> List[Location]:
409+
def find_items_in_locations(self, items: Set[str], player: int, resolve_group_locations: bool = False) -> List[Location]:
410+
if resolve_group_locations:
411+
player_groups = self.get_player_groups(player)
412+
return [location for location in self.get_locations() if
413+
location.item and location.item.name in items and location.player not in player_groups and
414+
(location.item.player == player or location.item.player in player_groups)]
405415
return [location for location in self.get_locations() if
406416
location.item and location.item.name in items and location.item.player == player]
407417

CommonClient.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ def __init__(self, server_address: typing.Optional[str], password: typing.Option
193193
self.hint_cost = None
194194
self.slot_info = {}
195195
self.permissions = {
196-
"forfeit": "disabled",
196+
"release": "disabled",
197197
"collect": "disabled",
198198
"remaining": "disabled",
199199
}
@@ -260,7 +260,7 @@ def reset_server_state(self):
260260
self.server_task = None
261261
self.hint_cost = None
262262
self.permissions = {
263-
"forfeit": "disabled",
263+
"release": "disabled",
264264
"collect": "disabled",
265265
"remaining": "disabled",
266266
}
@@ -821,6 +821,10 @@ async def server_auth(self, password_requested: bool = False):
821821
def on_package(self, cmd: str, args: dict):
822822
if cmd == "Connected":
823823
self.game = self.slot_info[self.slot].game
824+
825+
async def disconnect(self, allow_autoreconnect: bool = False):
826+
self.game = ""
827+
await super().disconnect(allow_autoreconnect)
824828

825829

826830
async def main(args):

Main.py

+4
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
122122
logger.info('Creating Items.')
123123
AutoWorld.call_all(world, "create_items")
124124

125+
# All worlds should have finished creating all regions, locations, and entrances.
126+
# Recache to ensure that they are all visible for locality rules.
127+
world._recache()
128+
125129
logger.info('Calculating Access Rules.')
126130

127131
for player in world.player_ids:

MultiServer.py

+54-11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import argparse
44
import asyncio
5+
import copy
56
import functools
67
import logging
78
import zlib
@@ -22,6 +23,9 @@
2223

2324
ModuleUpdate.update()
2425

26+
if typing.TYPE_CHECKING:
27+
import ssl
28+
2529
import websockets
2630
import colorama
2731
try:
@@ -40,6 +44,28 @@
4044
print_command_compatability_threshold = Version(0, 3, 5) # Remove backwards compatibility around 0.3.7
4145
colorama.init()
4246

47+
48+
def remove_from_list(container, value):
49+
try:
50+
container.remove(value)
51+
except ValueError:
52+
pass
53+
return container
54+
55+
56+
def pop_from_container(container, value):
57+
try:
58+
container.pop(value)
59+
except ValueError:
60+
pass
61+
return container
62+
63+
64+
def update_dict(dictionary, entries):
65+
dictionary.update(entries)
66+
return dictionary
67+
68+
4369
# functions callable on storable data on the server by clients
4470
modify_functions = {
4571
"add": operator.add, # add together two objects, using python's "+" operator (works on strings and lists as append)
@@ -56,6 +82,10 @@
5682
"and": operator.and_,
5783
"left_shift": operator.lshift,
5884
"right_shift": operator.rshift,
85+
# lists/dicts
86+
"remove": remove_from_list,
87+
"pop": pop_from_container,
88+
"update": update_dict,
5989
}
6090

6191

@@ -116,7 +146,6 @@ class Context:
116146
"location_check_points": int,
117147
"server_password": str,
118148
"password": str,
119-
"forfeit_mode": str, # TODO remove around 0.4
120149
"release_mode": str,
121150
"remaining_mode": str,
122151
"collect_mode": str,
@@ -138,7 +167,7 @@ class Context:
138167
non_hintable_names: typing.Dict[str, typing.Set[str]]
139168

140169
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
141-
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
170+
hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled",
142171
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
143172
log_network: bool = False):
144173
super(Context, self).__init__()
@@ -154,7 +183,7 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo
154183
self.player_names: typing.Dict[team_slot, str] = {}
155184
self.player_name_lookup: typing.Dict[str, team_slot] = {}
156185
self.connect_names = {} # names of slots clients can connect to
157-
self.allow_forfeits = {}
186+
self.allow_releases = {}
158187
# player location_id item_id target_player_id
159188
self.locations = {}
160189
self.host = host
@@ -171,7 +200,7 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo
171200
self.location_check_points = location_check_points
172201
self.hints_used = collections.defaultdict(int)
173202
self.hints: typing.Dict[team_slot, typing.Set[NetUtils.Hint]] = collections.defaultdict(set)
174-
self.release_mode: str = forfeit_mode
203+
self.release_mode: str = release_mode
175204
self.remaining_mode: str = remaining_mode
176205
self.collect_mode: str = collect_mode
177206
self.item_cheat = item_cheat
@@ -545,7 +574,7 @@ def set_save(self, savedata: dict):
545574
self.location_check_points = savedata["game_options"]["location_check_points"]
546575
self.server_password = savedata["game_options"]["server_password"]
547576
self.password = savedata["game_options"]["password"]
548-
self.release_mode = savedata["game_options"]["release_mode"]
577+
self.release_mode = savedata["game_options"].get("release_mode", savedata["game_options"].get("forfeit_mode", "goal"))
549578
self.remaining_mode = savedata["game_options"]["remaining_mode"]
550579
self.collect_mode = savedata["game_options"]["collect_mode"]
551580
self.item_cheat = savedata["game_options"]["item_cheat"]
@@ -590,6 +619,8 @@ def slot_set(self, slot) -> typing.Set[int]:
590619

591620
def _set_options(self, server_options: dict):
592621
for key, value in server_options.items():
622+
if key == "forfeit_mode":
623+
key = "release_mode"
593624
data_type = self.simple_options.get(key, None)
594625
if data_type is not None:
595626
if value not in {False, True, None}: # some can be boolean OR text, such as password
@@ -1226,7 +1257,7 @@ def _cmd_status(self, tag:str="") -> bool:
12261257

12271258
def _cmd_release(self) -> bool:
12281259
"""Sends remaining items in your world to their recipients."""
1229-
if self.ctx.allow_forfeits.get((self.client.team, self.client.slot), False):
1260+
if self.ctx.allow_releases.get((self.client.team, self.client.slot), False):
12301261
release_player(self.ctx, self.client.team, self.client.slot)
12311262
return True
12321263
if "enabled" in self.ctx.release_mode:
@@ -1735,7 +1766,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
17351766
return
17361767
args["cmd"] = "SetReply"
17371768
value = ctx.stored_data.get(args["key"], args.get("default", 0))
1738-
args["original_value"] = value
1769+
args["original_value"] = copy.copy(value)
17391770
for operation in args["operations"]:
17401771
func = modify_functions[operation["operation"]]
17411772
value = func(value, operation["value"])
@@ -1882,7 +1913,7 @@ def _cmd_allow_release(self, player_name: str) -> bool:
18821913
player = self.resolve_player(player_name)
18831914
if player:
18841915
team, slot, name = player
1885-
self.ctx.allow_forfeits[(team, slot)] = True
1916+
self.ctx.allow_releases[(team, slot)] = True
18861917
self.output(f"Player {name} is now allowed to use the !release command at any time.")
18871918
return True
18881919

@@ -1895,7 +1926,7 @@ def _cmd_forbid_release(self, player_name: str) -> bool:
18951926
player = self.resolve_player(player_name)
18961927
if player:
18971928
team, slot, name = player
1898-
self.ctx.allow_forfeits[(team, slot)] = False
1929+
self.ctx.allow_releases[(team, slot)] = False
18991930
self.output(f"Player {name} has to follow the server restrictions on use of the !release command.")
19001931
return True
19011932

@@ -2050,7 +2081,7 @@ def attrtype(input_text: str):
20502081
return input_text
20512082
setattr(self.ctx, option_name, attrtype(option))
20522083
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
2053-
if option_name in {"forfeit_mode", "release_mode", "remaining_mode", "collect_mode"}: # TODO remove forfeit_mode with 0.4
2084+
if option_name in {"release_mode", "remaining_mode", "collect_mode"}:
20542085
self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}])
20552086
elif option_name in {"hint_cost", "location_check_points"}:
20562087
self.ctx.broadcast_all([{"cmd": "RoomUpdate", option_name: getattr(self.ctx, option_name)}])
@@ -2090,6 +2121,8 @@ def parse_args() -> argparse.Namespace:
20902121
parser.add_argument('--password', default=defaults["password"])
20912122
parser.add_argument('--savefile', default=defaults["savefile"])
20922123
parser.add_argument('--disable_save', default=defaults["disable_save"], action='store_true')
2124+
parser.add_argument('--cert', help="Path to a SSL Certificate for encryption.")
2125+
parser.add_argument('--cert_key', help="Path to SSL Certificate Key file")
20932126
parser.add_argument('--loglevel', default=defaults["loglevel"],
20942127
choices=['debug', 'info', 'warning', 'error', 'critical'])
20952128
parser.add_argument('--location_check_points', default=defaults["location_check_points"], type=int)
@@ -2162,6 +2195,14 @@ async def auto_shutdown(ctx, to_cancel=None):
21622195
await asyncio.sleep(seconds)
21632196

21642197

2198+
def load_server_cert(path: str, cert_key: typing.Optional[str]) -> "ssl.SSLContext":
2199+
import ssl
2200+
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
2201+
ssl_context.load_default_certs()
2202+
ssl_context.load_cert_chain(path, cert_key if cert_key else path)
2203+
return ssl_context
2204+
2205+
21652206
async def main(args: argparse.Namespace):
21662207
Utils.init_logging("Server", loglevel=args.loglevel.lower())
21672208

@@ -2197,8 +2238,10 @@ async def main(args: argparse.Namespace):
21972238

21982239
ctx.init_save(not args.disable_save)
21992240

2241+
ssl_context = load_server_cert(args.cert, args.cert_key) if args.cert else None
2242+
22002243
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), host=ctx.host, port=ctx.port, ping_timeout=None,
2201-
ping_interval=None)
2244+
ping_interval=None, ssl=ssl_context)
22022245
ip = args.host if args.host else Utils.get_public_ipv4()
22032246
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
22042247
'No password' if not ctx.password else 'Password: %s' % ctx.password))

NetUtils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class Permission(enum.IntFlag):
4343
disabled = 0b000 # 0, completely disables access
4444
enabled = 0b001 # 1, allows manual use
4545
goal = 0b010 # 2, allows manual use after goal completion
46-
auto = 0b110 # 6, forces use after goal completion, only works for forfeit
46+
auto = 0b110 # 6, forces use after goal completion, only works for release
4747
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
4848

4949
@staticmethod

Starcraft2Client.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import sys
1111
import typing
1212
import queue
13+
import zipfile
14+
import io
1315
from pathlib import Path
1416

1517
# CommonClient import first to trigger ModuleUpdater
@@ -120,9 +122,9 @@ def _cmd_set_path(self, path: str = '') -> bool:
120122
sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.")
121123
return False
122124

123-
def _cmd_download_data(self, force: bool = False) -> bool:
125+
def _cmd_download_data(self) -> bool:
124126
"""Download the most recent release of the necessary files for playing SC2 with
125-
Archipelago. force should be True or False. force=True will overwrite your files."""
127+
Archipelago. Will overwrite existing files."""
126128
if "SC2PATH" not in os.environ:
127129
check_game_install_path()
128130

@@ -132,11 +134,11 @@ def _cmd_download_data(self, force: bool = False) -> bool:
132134
else:
133135
current_ver = None
134136

135-
tempzip, version = download_latest_release_zip('TheCondor07', 'Starcraft2ArchipelagoData', current_version=current_ver, force_download=force)
137+
tempzip, version = download_latest_release_zip('TheCondor07', 'Starcraft2ArchipelagoData',
138+
current_version=current_ver, force_download=True)
136139

137140
if tempzip != '':
138141
try:
139-
import zipfile
140142
zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"])
141143
sc2_logger.info(f"Download complete. Version {version} installed.")
142144
with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "w") as f:
@@ -195,12 +197,16 @@ def on_package(self, cmd: str, args: dict):
195197
self.build_location_to_mission_mapping()
196198

197199
# Looks for the required maps and mods for SC2. Runs check_game_install_path.
198-
is_mod_installed_correctly()
200+
maps_present = is_mod_installed_correctly()
199201
if os.path.exists(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt"):
200202
with open(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt", "r") as f:
201203
current_ver = f.read()
202204
if is_mod_update_available("TheCondor07", "Starcraft2ArchipelagoData", current_ver):
203205
sc2_logger.info("NOTICE: Update for required files found. Run /download_data to install.")
206+
elif maps_present:
207+
sc2_logger.warning("NOTICE: Your map files may be outdated (version number not found). "
208+
"Run /download_data to update them.")
209+
204210

205211
def on_print_json(self, args: dict):
206212
# goes to this world
@@ -1003,7 +1009,7 @@ def download_latest_release_zip(owner: str, repo: str, current_version: str = No
10031009
download_url = r1.json()["assets"][0]["browser_download_url"]
10041010

10051011
r2 = requests.get(download_url, headers=headers)
1006-
if r2.status_code == 200:
1012+
if r2.status_code == 200 and zipfile.is_zipfile(io.BytesIO(r2.content)):
10071013
with open(f"{repo}.zip", "wb") as fh:
10081014
fh.write(r2.content)
10091015
sc2_logger.info(f"Successfully downloaded {repo}.zip.")

Utils.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class Version(typing.NamedTuple):
3838
build: int
3939

4040

41-
__version__ = "0.3.7"
41+
__version__ = "0.3.8"
4242
version_tuple = tuplize_version(__version__)
4343

4444
is_linux = sys.platform.startswith("linux")
@@ -505,7 +505,7 @@ def _cleanup():
505505
except Exception as e:
506506
logging.exception(e)
507507
else:
508-
logging.info(f"Deleted old logfile {file.path}")
508+
logging.debug(f"Deleted old logfile {file.path}")
509509
import threading
510510
threading.Thread(target=_cleanup, name="LogCleaner").start()
511511
import platform

WebHostLib/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
app.config["SELFHOST"] = True # application process is in charge of running the websites
2525
app.config["GENERATORS"] = 8 # maximum concurrent world gens
2626
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
27+
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
28+
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
2729
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
2830
app.config["DEBUG"] = False
2931
app.config["PORT"] = 80

WebHostLib/autolauncher.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -177,14 +177,17 @@ def __init__(self, room: Room, config: dict):
177177
with guardian_lock:
178178
multiworlds[self.room_id] = self
179179
self.ponyconfig = config["PONY"]
180+
self.cert = config["SELFLAUNCHCERT"]
181+
self.key = config["SELFLAUNCHKEY"]
180182

181183
def start(self):
182184
if self.process and self.process.is_alive():
183185
return False
184186

185187
logging.info(f"Spinning up {self.room_id}")
186188
process = multiprocessing.Process(group=None, target=run_server_process,
187-
args=(self.room_id, self.ponyconfig, get_static_server_data()),
189+
args=(self.room_id, self.ponyconfig, get_static_server_data(),
190+
self.cert, self.key),
188191
name="MultiHost")
189192
process.start()
190193
# bind after start to prevent thread sync issues with guardian.

0 commit comments

Comments
 (0)