From 9a2ab0287d2324f1f5a645125b5c7ef534324cd3 Mon Sep 17 00:00:00 2001 From: DodoBirb Date: Tue, 14 Mar 2023 18:00:11 +1100 Subject: [PATCH 001/282] Added empty AM2RClient --- AM2RClient.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 AM2RClient.py diff --git a/AM2RClient.py b/AM2RClient.py new file mode 100644 index 000000000000..f0963b468bb6 --- /dev/null +++ b/AM2RClient.py @@ -0,0 +1,71 @@ +import asyncio +import copy +import json +import time +from asyncio import StreamReader, StreamWriter +from typing import List + +import Utils +from Utils import async_start +from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \ + get_base_parser + +class AM2RCommandProcessor(ClientCommandProcessor): + def __init__(self, ctx: CommonContext): + super().__init__(ctx) + +class AM2RContext(CommonContext): + command_processor = AM2RCommandProcessor + game = 'AM2R' + items_handling = 0b001 # full local + + def __init__(self, server_address, password): + super().__init__(server_address, password) + self.waiting_for_client = False + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super().server_auth(password_requested) + if not self.auth: + self.waiting_for_client = True + logger.info('Awaiting connection to AM2R to get Player information') + return + + await self.send_connect() + + def run_gui(self): + from kvui import GameManager + + class AM2RManager(GameManager): + logging_pairs = [ + ("Client", "Multiworld") + ] + base_title = "AM2R Multiworld Client" + + self.ui = AM2RManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + +if __name__ == '__main__': + # Text Mode to use !hint and such with games that have no text entry + Utils.init_logging("AM2RClient") + + options = Utils.get_options() + + async def main(args): + ctx = AM2RContext(args.connect, args.password) + 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() + ctx.server_address = None + + await ctx.shutdown() + + import colorama + + parser = get_base_parser() + args = parser.parse_args() + colorama.init() + asyncio.run(main(args)) + colorama.deinit() \ No newline at end of file From ac466e04c381412aa3b97b934dedc928fecaf4e8 Mon Sep 17 00:00:00 2001 From: DodoBirb Date: Tue, 14 Mar 2023 20:21:18 +1100 Subject: [PATCH 002/282] Established connection with AM2R --- AM2RClient.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/AM2RClient.py b/AM2RClient.py index f0963b468bb6..32bc48fe5782 100644 --- a/AM2RClient.py +++ b/AM2RClient.py @@ -10,6 +10,16 @@ from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \ get_base_parser +CONNECTION_TIMING_OUT_STATUS = "Connection timing out" +CONNECTION_REFUSED_STATUS = "Connection Refused" +CONNECTION_RESET_STATUS = "Connection was reset" +CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" +CONNECTION_CONNECTED_STATUS = "Connected" +CONNECTION_INITIAL_STATUS = "Connection has not been initiated" + + + + class AM2RCommandProcessor(ClientCommandProcessor): def __init__(self, ctx: CommonContext): super().__init__(ctx) @@ -22,6 +32,9 @@ class AM2RContext(CommonContext): def __init__(self, server_address, password): super().__init__(server_address, password) self.waiting_for_client = False + self.am2r_streams: (StreamReader, StreamWriter) = None + self.am2r_sync_task = None + self.am2r_status = CONNECTION_INITIAL_STATUS async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -45,6 +58,24 @@ class AM2RManager(GameManager): self.ui = AM2RManager(self) self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") +async def am2r_sync_task(ctx: AM2RContext): + logger.info("Starting AM2R connector") + while not ctx.exit_event.is_set(): + error_status = None + try: + logger.debug("Attempting to connect to AM2R") + ctx.am2r_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 64197), timeout=10) + ctx.am2r_status = CONNECTION_TENTATIVE_STATUS + except TimeoutError: + logger.debug("Connection Timed Out, Trying Again") + ctx.am2r_status = CONNECTION_TIMING_OUT_STATUS + continue + except ConnectionRefusedError: + logger.debug("Connection Refused, Trying Again") + ctx.am2r_status = CONNECTION_REFUSED_STATUS + continue + + if __name__ == '__main__': # Text Mode to use !hint and such with games that have no text entry Utils.init_logging("AM2RClient") @@ -57,6 +88,7 @@ async def main(args): if gui_enabled: ctx.run_gui() ctx.run_cli() + ctx.am2r_sync_task = asyncio.create_task(am2r_sync_task(ctx), name="AM2R Sync") await ctx.exit_event.wait() ctx.server_address = None From 7188cefaedadaba7925dfada13a8f19382fba027 Mon Sep 17 00:00:00 2001 From: DodoBirb Date: Thu, 16 Mar 2023 08:23:19 +1100 Subject: [PATCH 003/282] AM2RClient can now send and receive data --- AM2RClient.py | 75 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 13 deletions(-) diff --git a/AM2RClient.py b/AM2RClient.py index 32bc48fe5782..7cb6f813ff7c 100644 --- a/AM2RClient.py +++ b/AM2RClient.py @@ -24,6 +24,11 @@ class AM2RCommandProcessor(ClientCommandProcessor): def __init__(self, ctx: CommonContext): super().__init__(ctx) + def _cmd_am2r(self): + """Check AM2R Connection State""" + if isinstance(self.ctx, AM2RContext): + logger.info(f"Connection Status: {self.ctx.am2r_status}") + class AM2RContext(CommonContext): command_processor = AM2RCommandProcessor game = 'AM2R' @@ -59,21 +64,65 @@ class AM2RManager(GameManager): self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") async def am2r_sync_task(ctx: AM2RContext): - logger.info("Starting AM2R connector") + logger.info("Starting AM2R connector, use /am2r for status information.") while not ctx.exit_event.is_set(): error_status = None - try: - logger.debug("Attempting to connect to AM2R") - ctx.am2r_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 64197), timeout=10) - ctx.am2r_status = CONNECTION_TENTATIVE_STATUS - except TimeoutError: - logger.debug("Connection Timed Out, Trying Again") - ctx.am2r_status = CONNECTION_TIMING_OUT_STATUS - continue - except ConnectionRefusedError: - logger.debug("Connection Refused, Trying Again") - ctx.am2r_status = CONNECTION_REFUSED_STATUS - continue + if ctx.am2r_streams: + (reader, writer) = ctx.am2r_streams + msg = "HelloWorld".encode() + writer.write(msg) + writer.write(b'\n') + try: + print("Hi") + await asyncio.wait_for(writer.drain(), timeout=1.5) + print("Ho") + try: + data = await asyncio.wait_for(reader.readline(), timeout=5) + data_decoded = data.decode() + logger.info(data_decoded) + except asyncio.TimeoutError: + logger.debug("Read Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.am2r_streams = None + except ConnectionResetError as e: + logger.debug("Read failed due to Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.am2r_streams = None + except TimeoutError: + logger.debug("Connection Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.am2r_streams = None + except ConnectionResetError: + logger.debug("Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.am2r_streams = None + + if ctx.am2r_status == CONNECTION_TENTATIVE_STATUS: + if not error_status: + logger.info("Successfully Connected to AM2R") + ctx.am2r_status = CONNECTION_CONNECTED_STATUS + else: + ctx.am2r_status = f"Was tentatively connected but error occured: {error_status}" + elif error_status: + ctx.am2r_status = error_status + logger.info("Lost connection to AM2R and attempting to reconnect. Use /am2r for status updates") + else: + try: + logger.debug("Attempting to connect to AM2R") + ctx.am2r_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 64197), timeout=10) + ctx.am2r_status = CONNECTION_TENTATIVE_STATUS + except TimeoutError: + logger.debug("Connection Timed Out, Trying Again") + ctx.am2r_status = CONNECTION_TIMING_OUT_STATUS + continue + except ConnectionRefusedError: + logger.debug("Connection Refused, Trying Again") + ctx.am2r_status = CONNECTION_REFUSED_STATUS + continue if __name__ == '__main__': From 552c87462bc868df2041ce60eb9dd898bf9233c0 Mon Sep 17 00:00:00 2001 From: Ehseezed Date: Mon, 30 Oct 2023 19:27:35 -0500 Subject: [PATCH 004/282] Genesis --- worlds/am2r/locations.py | 2 +- worlds/am2r/regions.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/worlds/am2r/locations.py b/worlds/am2r/locations.py index 71290734aa53..e03644580884 100644 --- a/worlds/am2r/locations.py +++ b/worlds/am2r/locations.py @@ -88,7 +88,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int]): LocationData("GFS Thoth", "GFS Thoth: Research Camp", 8680056), LocationData("GFS Thoth", "GFS Thoth: Hornoad room", 8680057, lambda state: state.has("Power Bomb", player)), LocationData("GFS Thoth", "GFS Thoth: Outside the Front of the Ship", 8680058, lambda state: state.has("Power Bomb", player)), - LocationData("GFS Thoth", "GFS Thoth: Genesis", 8680059, lambda state: state.has("Power Bomb", player)), + LocationData("Genesis", "Genesis: Boss", 8680059, lambda state: state.has("Power Bomb", player)), LocationData("The Tower", "The Tower: Beside Hydro Pipe", 8680060, lambda state: state.has("Screw Attack", player)), LocationData("The Tower", "The Tower: Right Side of Tower", 8680061), diff --git a/worlds/am2r/regions.py b/worlds/am2r/regions.py index b04e543d20bf..a2f842e15df9 100644 --- a/worlds/am2r/regions.py +++ b/worlds/am2r/regions.py @@ -95,6 +95,9 @@ def create_regions_and_locations(world: MultiWorld, player: int): connect(world, player, "Main Caves", "GFS Thoth"), connect(world, player, "GFS Thoth", "Main Caves"), + connect(world, player, "GFS Thoth", "Genesis") + connect(world, player, "GFS Thoth", "Genesis") + connect(world, player, "Guardian", "Golden Temple"), connect(world, player, "Golden Temple", "Guardian"), From 5282c9700a00bb22f5bc20f4b0a128d537bbd738 Mon Sep 17 00:00:00 2001 From: Ehseezed Date: Mon, 30 Oct 2023 23:13:59 -0500 Subject: [PATCH 005/282] clear out pycache --- .../am2r/__pycache__/__init__.cpython-310.pyc | Bin 3080 -> 0 bytes .../am2r/__pycache__/locations.cpython-310.pyc | Bin 14995 -> 0 bytes worlds/am2r/__pycache__/options.cpython-310.pyc | Bin 2212 -> 0 bytes worlds/am2r/__pycache__/regions.cpython-310.pyc | Bin 9027 -> 0 bytes worlds/am2r/__pycache__/rules.cpython-310.pyc | Bin 2873 -> 0 bytes 5 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 worlds/am2r/__pycache__/__init__.cpython-310.pyc delete mode 100644 worlds/am2r/__pycache__/locations.cpython-310.pyc delete mode 100644 worlds/am2r/__pycache__/options.cpython-310.pyc delete mode 100644 worlds/am2r/__pycache__/regions.cpython-310.pyc delete mode 100644 worlds/am2r/__pycache__/rules.cpython-310.pyc diff --git a/worlds/am2r/__pycache__/__init__.cpython-310.pyc b/worlds/am2r/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 04271c4e8b48866d03cabb38de8617e9b99c2552..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3080 zcmZ`*TW=dT7Uo=ZwIs`zxJ?_h({{UC0jb)i`?T-|b(7X8S{p?ZpuoV30X?MTab`3s zNhP(AeX@Zq_O*W?er$gdU-v0LV1Y%R_B)hh$w@i`a>yZhuHQL_WxL&Ec>eqQ-{tcf zWB;Ma>Jwt}1)9DEpWBW`rMDv1)rtF>v@doB=qJ?=&u4BF_+7DQ- z^Bj_}o@c}RS!$kpvq>{EvPcX+&SlTHn?q@myiB>t%26V?;o8=|dMrog8-bIzQYE>O ziIPLC>x7TQ)ozXB^@kHvs*LA0{03xkqP2H$=ZB;m6Fu5d`LR^Fu=OA1u^Xi%)9M3T z&g}MQ3?z#^!D3%{aUesooCrllR7Gv*omYomToH9>*042V1DbJT%#}HNm=$umb1=zG z_JsIF52Trlqy0%H67UiTIqDZ;T6dFA!K#Razkzo{F`jxi@IoBgz=Wx5>2s&(Kg_} zfju_{p=gT^{_J3FU34*e1EUSGiP4*)0tju}hJz5HW%|EngvS8@KIZUT!5;ww0|$~c z=UQiJbRdl?GZEd1hHy?aD3x2@r^%P)87ACBMrBe*nbFa))R>mhBPnNK8%UWuMsvCd zoMATNdBk<}MCR^0$nu=ixzCL%=%Os09MWe!G}#0LCbNc8nYBitP5junmr znmFuuQMBu5pvr=T1(7+kzFUn$QaYv7uy}k7$Z7Y`^bWd=J!Su>{nLBuoia4h60)oH*^U>GqW$`pXT6ie$1? zg*Z{8YXZ_jFmDcw|6&7Ac*4J^|H4igVFvyU4zC>|URAU$H2pq0r&XJ@G7QJ?J@@ei zuug+j4MST)isZ&9+gt{?tCG{G=85= zZ!a|_e;;OFwt9!Ucd1(nEKRgR<`w%?aKzc?Qw(4k$RnIYm;SThDSL*hb?IHA?wiW9Dy@_CF6Y(*ET-%Fm#Ud@@@wpMGlQ`2q$j;W;5 z{78z#sZZ)DLS5XQtHS{`XQ$WttF@xE{siYV+5MIW@a(+SGWr$MTt($uIC(uPPdy5* zT?e6;W0O!vyj*OOk6}gJpC~HH$36d05qgkbf)lQ1e)VicBP!*JDh_q^-JBQ4g5Uis zPRu;ZrXMclN2yH|%BfxnwfPCanh@^*&V@@ zNhTDb(rzs>@5+kpF1HRr)qHA74HyC6RMG+Ek56T4)SrkUB@fl0&H;%6 zMSVt{i|}RV5*(EYYMS(;yFpl~dU*Y9w9wn}o55z#3_78rU5C9bTQz566e`EvsiX)2 zwz1GaJB<{jY+IY(K3&$aYhQ9L?@>vX+TDI;Qj))&y=-@OgX!<#PUNnfYK_)f)R7sT d46jqe)^}%jt-FLsP5H0IT3**%v~IB7`agHj`^W$Q diff --git a/worlds/am2r/__pycache__/locations.cpython-310.pyc b/worlds/am2r/__pycache__/locations.cpython-310.pyc deleted file mode 100644 index 1624c2a6e7c449b6f39e902471d7ad04fc5c6346..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14995 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 M0k@&1(DLg42g(}UCjbBd diff --git a/worlds/am2r/__pycache__/options.cpython-310.pyc b/worlds/am2r/__pycache__/options.cpython-310.pyc deleted file mode 100644 index c65cb155fe8a3c12e7be826dc7df71a043c4241c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2212 zcmZ`*OK%%D5Z;%bmMzInoVtgCeMdkOplOjqkrq+yHcb&f5GN?GFtDJPBWdg1r9yI@ z$Z$`|&*&dWe@)N4_LLvcOMwEN*_ABUNLJv8oEZ*3&TCh$uC^U~{{G`zxU=Rsf6?Od zvx1BJIP{-Lo;t!2Zt7%i=w@E%Wq#y720KY?2Ci-I7O6>okt!8~y6q0NhU_?n{i#7+ZR^_;nZ#gQJHXB4 zQIen7?hcGNZrabE$niJ@TYJp&G1&D(!?s1$z$WrJ%XmIx3k8+~2xcTZ z26mF9DK4a9^4RbsuLNVs%tXmzJRvnR0VXpTbKI!S+0xG9BsRkR`=!PnYAAyKW`Jbx zb<;MZDCZeOk!?p&CPk6byc0#w3ZBj-jVKZ_CbmfQH085$i(OIVtw@uzY^z#8E^I?o zZhh^j8+b}d{`qR|qA^skJ%RW{ zpWXY3p8^9OPnaB)UjOPk+SxJpdJBA@MQrL%i!y2+fk59j3{6 zV$vDTV@ai!pc11^B%bGWVjyEvtUvYxR?Re9s?1%$zGRqqLT0Q~rA#lTR25&bEXfN4 zy3OzrFvZMF@-f4BQNb)cSZ%wRNeoJ?5|hL{EqR(fN9|qRZ zz#0&z!oC8kJWoS9x&<$=R&q`)@XnH?#BGhj1WAY6o`Aco|W@S#qII%dK+ z8x@FP#PboQ4Biq4Z-l(ZJ%Nd4OXZhoT*G$C@kORp zjW~_{stGFUeyTx)45}6p`Z7@T0dOT|iXqSX*!XQXig=z&yaK%vv7!4|X*M{<0xR8A rBD#dV^mFOr=Dq6O@Evt4O{`742XlLVui0yPL(lcx&41rty&3!mW$`++ diff --git a/worlds/am2r/__pycache__/regions.cpython-310.pyc b/worlds/am2r/__pycache__/regions.cpython-310.pyc deleted file mode 100644 index 9f43a9558b92334951c9247374cba28d004f9389..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9027 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 diff --git a/worlds/am2r/__pycache__/rules.cpython-310.pyc b/worlds/am2r/__pycache__/rules.cpython-310.pyc deleted file mode 100644 index dae79697189a62af79fda0de7ff392c79d8a8f56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2873 zcmai0OK;mo5aupPk$Ty(k~)v3jr*=pB!^u3fD=S^fuun!1QCKFtO0`NZY0K}$m~)v zY$&JXke^|rC->Y#PyHdh_T+QWiwEF#G5*xC^g*0D@_Zgqnl@X-FdKP=dN1>XG3Xk?EK)HkcWfBFnKN+p(jvQzqIy zjg^@7Mq}1%>Qor%YAyQ&;%S{0$;>B%ARe@6R@oYcY4CF_LY6IW#$m|)6uxv*FXbl$ zyjyzK*y5>(112By7sEhsmaR-IZNWGD+~1X1qsLP>PH?jO(hG;&$F*i30|s~Dm4ATX znnRf8&?g$#xxvUf?2em^aQ&IVbY{FUoDws+#Y)@;i^XiPlxHkuRsl<8#!_W9uvBL( zb+!bS+KgqHHNaAzv8=ErSe9li7uZFxEYDazV5?whuuG7`%UQX-b?foNxEJ_YDG9xO zE;4fu6X+vvlLvJQ?Lso5_A=yKXQDQrE== zB|@r@gJ!XZo0HF(DUtN7BzQU$10Ovn59&xCZsuJR#E2Y1Su)y(O+6<^6gH)IGIJx2 zcH}|zQM|{+ni?F_%<6m6^#=R-{-}V8*F*ya?lc!mpk;Lg<@(;h-GMmx@f|MQBnOwK zaL@QG1SuyV`vCJ-Qh=nLJ4lSU0AgI}CZ5mN9t@*oY~2r(A}-?RRTK-+J%ygqqZWF~yd+hD*-N#&Q8ns+s5`OCXkrP3XS|SLPthJ^u%!b_hU^2mmC9 zgduZOl^5-Zs}R1Wi;of0M<}qO=b~#&(fR!-e#vpFf>$9mgm-lwyxesmz6o#RdN<*m zL4C(E<>H{)O++y0t!-fPCgmQ6i_xjW=i;fre9OcWyJ}C0+H2>KopC#Z4G*LVuj(+r zgOX~g$`>QA^;fhQQ5fkXo$0%#_#Uzf-4(MsqDOlCof?~YmSLnF8kl{hnWq}gDCHhW z?r%+4f!QTEVYWJ9z*GDbB3No3YI%gGYK=*Rd{jhel{*5On1!@K&uciI9(q2(9W&^_<{k zae^fzK0|>8A-+I?%&6KDU!nFjiWZ7ZILLJnIj1&xpH*9b-*%4ad7T=9 zcUmiWF%P_myKYu--6&>oVS`?C-4{bIJpG~vgEU)q;rblJa67|~E9tsTLoBHXW(KMR+YEAjH zs#!UJo|nRcGApI~2{b-|dC#gFp5&XMCnc9TK!wf9e-g@>L#~b_t~d`$X0PX|xPw1F P)ek3It3mEBlBWJ2&i{xJ From a00613045e2dfbfeb22a69da6e2eddfb8b0c54eb Mon Sep 17 00:00:00 2001 From: Ehseezed Date: Mon, 30 Oct 2023 23:53:19 -0500 Subject: [PATCH 006/282] Logic Fix --- worlds/am2r/locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/am2r/locations.py b/worlds/am2r/locations.py index 5dfc54419da5..0aca6103ca4e 100644 --- a/worlds/am2r/locations.py +++ b/worlds/am2r/locations.py @@ -18,7 +18,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int]): logic = AM2RLogic(world, player) location_table: List[LocationData] = [ - LocationData("Main Caves", "Main Caves: Vertical Spike Room Upper", 8680000, lambda state: logic.AM2R_can_fly(state) or state.has("Spider Ball", player) and state.has("Bombs", player)), # spider + bomb + LocationData("Main Caves", "Main Caves: Vertical Spike Room Upper", 8680000, lambda state: logic.AM2R_can_fly(state) and logic.AM2R_can_bomb(state)), # spider + bomb LocationData("Main Caves", "Main Caves: Vertical Spike Room Lower", 8680001, logic.AM2R_can_bomb), # bomb LocationData("Main Caves", "Main Caves: Crumble Spike Room", 8680002, lambda state: state.has_any({'Hi Jump', 'Space Jump'}, player) and logic.AM2R_can_bomb(state)), # jump. just jump. ibj and dbj can come later in advanced logic frogtroll LocationData("Main Caves", "Main Caves: Maze", 8680003), From e6eff51d703e1c4fb8daabb0ce75b736ccd8ff9b Mon Sep 17 00:00:00 2001 From: Ehseezed Date: Sat, 4 Nov 2023 14:59:52 -0500 Subject: [PATCH 007/282] Better handling of metroids not being checks leaves locations in the pool but forces a "Metroid" item to them --- worlds/am2r/__init__.py | 51 +++++++++++++++- worlds/am2r/locations.py | 125 +++++++++++++++++++-------------------- 2 files changed, 110 insertions(+), 66 deletions(-) diff --git a/worlds/am2r/__init__.py b/worlds/am2r/__init__.py index 021fafd1abdb..489cb5d426ca 100644 --- a/worlds/am2r/__init__.py +++ b/worlds/am2r/__init__.py @@ -3,7 +3,7 @@ from .locations import get_location_datas, EventId from .regions import create_regions_and_locations from BaseClasses import Tutorial, Item -from .options import AM2R_options +from .options import AM2R_options, MetroidsAreChecks from worlds.AutoWorld import World, WebWorld @@ -45,6 +45,55 @@ def create_item(self, name: str) -> Item: return items.create_item(self.player, name) def create_items(self) -> None: + if self.options.MetroidsAreChecks != MetroidsAreChecks.option_include_A6: + self.multiworld.get_location("Deep Caves: Little Bro", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Deep Caves: Big Sis", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Omega Nest: SA-X Queen Lucina", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Omega Nest: Epsilon", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Omega Nest: Druid", self.player).place_locked_item(self.create_item("Metroid")) + if self.options.MetroidsAreChecks == MetroidsAreChecks.option_disabled: + self.multiworld.get_location("The Forgotten Alpha", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Golden Temple: Metroid above Spider Ball", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Golden Temple Nest: Moe", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Golden Temple Nest: Larry", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Golden Temple Nest: Curly", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Main Caves: Freddy Fazbear", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Hydro Station: Turbine Terror", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Hydro Station: The Lookout", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Hydro Station: Recent Guardian", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Hydro Nest: Spiderman Decent", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Hydro Nest: Carnage Awful", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Hydro Nest: Venom Awesome", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Hydro Nest: Something More Something Awesome",self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Industrial Nest: Mimolette", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Industrial Nest: The Big Cheese", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Industrial Nest: Mohwir", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Industrial Nest: Chirn", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Industrial Nest: BHHarbinger", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Industrial Nest: The Abyssal Creature", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Industrial Complex: Sisyphus", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Industrial Complex: And then there\'s this Asshole",self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Inside Industrial: Guardian of Doom Treadmill",self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Inside Industrial: Rawsome1234 by the Lava",self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Dual Alphas: Marco", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Dual Alphas: Polo", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Mines: Unga", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Mines: Gunga", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("The Tower: Patricia", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("The Tower: Variable \"GUH\"", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Ruler of The Tower: Slagathor", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("The Tower: Anakin", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("The Tower: Mr.Sandman", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("The Tower: Xander", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("EMP: Sir Zeta Commander of the Alpha Squadron", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Alpha Squadron: Timmy", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Alpha Squadron: Tommy", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Alpha Squadron: Terry", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Alpha Squadron: Telly", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Alpha Squadron: Martin", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Underwater: Gamma Bros Mario", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Underwater: Gamma Bros Luigi", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("The Last Metroid is in Captivity", self.player).place_locked_item(self.create_item("The Galaxy is at Peace")) items.create_all_items(self.multiworld, self.player) diff --git a/worlds/am2r/locations.py b/worlds/am2r/locations.py index 0aca6103ca4e..57d75655d61c 100644 --- a/worlds/am2r/locations.py +++ b/worlds/am2r/locations.py @@ -128,71 +128,66 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int]): LocationData("Deep Caves", "Deep Caves: After Omega", 8680088), LocationData("Research Station", "The Last Metroid is in Captivity", EventId), - ] - if not world or is_option_enabled(world, player, "MetroidsAreChecks"): - location_table += ( - #metroids - # todo remove or place locked items below when option not enabled - LocationData("First Alpha", "The Forgotten Alpha", 8680100), - - LocationData("Golden Temple", "Golden Temple: Metroid above Spider Ball", 8680101, logic.AM2R_can_spider), - LocationData("Golden Temple Nest", "Golden Temple Nest: Moe", 8680102, logic.AM2R_can_bomb), # Loj - LocationData("Golden Temple Nest", "Golden Temple Nest: Larry", 8680103, logic.AM2R_can_bomb), # Loj - LocationData("Golden Temple Nest", "Golden Temple Nest: Curly", 8680104, logic.AM2R_can_bomb), # Loj - - LocationData("Main Caves", "Main Caves: Freddy Fazbear", 8680105), # Epsilon - LocationData("Hydro Station", "Hydro Station: Turbine Terror", 8680106), # Xander - LocationData("Hydro Station", "Hydro Station: The Lookout", 8680107, logic.AM2R_can_schmove), # Xander - LocationData("Hydro Station", "Hydro Station: Recent Guardian", 8680108), # ANX - - LocationData("Hydro Nest", "Hydro Nest: Spiderman Decent", 8680109), - LocationData("Hydro Nest", "Hydro Nest: Carnage Awful", 8680110), - LocationData("Hydro Nest", "Hydro Nest: Venom Awesome", 8680111), - LocationData("Hydro Nest", "Hydro Nest: Something More Something Awesome", 8680112), - - LocationData("Industrial Complex Nest", "Industrial Nest: Mimolette", 8680113, lambda state: state.has("Speed Booster", player) or state.has("Super Missile", player)), - LocationData("Industrial Complex Nest", "Industrial Nest: The Big Cheese", 8680114, lambda state: state.has("Speed Booster", player) or state.has("Super Missile", player)), - LocationData("Industrial Complex Nest", "Industrial Nest: Mohwir", 8680115, lambda state: logic.AM2R_can_bomb(state) and (state.has("Speed Booster", player) or state.has("Super Missile", player))), - LocationData("Industrial Complex Nest", "Industrial Nest: Chirn", 8680116, lambda state: logic.AM2R_can_bomb(state) and (state.has("Speed Booster", player) or state.has("Super Missile", player))), - LocationData("Industrial Complex Nest", "Industrial Nest: BHHarbinger", 8680117, lambda state: logic.AM2R_can_bomb(state) and (state.has("Speed Booster", player) or state.has("Super Missile", player))), - LocationData("Industrial Complex Nest", "Industrial Nest: The Abyssal Creature", 8680118, lambda state: logic.AM2R_can_bomb(state) and state.has("Spider Ball", player) and (state.has("Speed Booster", player) or state.has("Super Missile", player))), - - LocationData("Pre Industrial Complex", "Industrial Complex: Sisyphus", 8680119, logic.AM2R_can_spider), # Mimo - LocationData("Pre Industrial Complex", "Industrial Complex: And then there\'s this Asshole", 8680120, logic.AM2R_can_spider), # ANX - - LocationData("Industrial Complex", "Inside Industrial: Guardian of Doom Treadmill", 8680121, lambda state: state.has("Speed Booster", player) and logic.AM2R_can_bomb(state)), - LocationData("Industrial Complex", "Inside Industrial: Rawsome1234 by the Lava", 8680122, lambda state: state.has("Speed Booster", player) and logic.AM2R_can_bomb(state)), - - LocationData("GFS Thoth", "Dual Alphas: Marco", 8680123), # Epsilon - LocationData("GFS Thoth", "Dual Alphas: Polo", 8680124), # Epsilon - - LocationData("Mines", "Mines: Unga", 8680125, lambda state: state.has("Super Missile", player) and (state.has("Space Jump", player) or state.has("Spider Ball", player))), - LocationData("Mines", "Mines: Gunga", 8680126, lambda state: state.has("Super Missile", player) and (state.has("Space Jump", player) or state.has("Spider Ball", player))), - - LocationData("The Tower", "The Tower: Patricia", 8680127, logic.AM2R_can_fly), # Mahan - LocationData("The Tower", "The Tower: Variable \"GUH\"", 8680128, logic.AM2R_can_fly), # ANX - LocationData("The Tower", "Ruler of The Tower: Slagathor", 8680129, logic.AM2R_can_schmove), # Rawsome - LocationData("The Tower", "The Tower: Anakin", 8680130, logic.AM2R_can_bomb), # Xander - LocationData("The Tower", "The Tower: Mr.Sandman", 8680131, logic.AM2R_can_fly), # Xander - LocationData("The Tower", "The Tower: Xander", 8680132, lambda state: state.has("Space Jump", player)), - - LocationData("EMP", "EMP: Sir Zeta Commander of the Alpha Squadron", 8680133, logic.AM2R_can_bomb), # Lucina - - LocationData("Pipe Hell R", "Alpha Squadron: Timmy", 8680134), # Lucina - LocationData("Pipe Hell R", "Alpha Squadron: Tommy", 8680135), # Lucina - LocationData("Pipe Hell R", "Alpha Squadron: Terry", 8680136), # Lucina - LocationData("Pipe Hell R", "Alpha Squadron: Telly", 8680137), # Lucina - LocationData("Pipe Hell R", "Alpha Squadron: Martin", 8680138), - - LocationData("Underwater Distro Connection", "Underwater: Gamma Bros Mario", 8680139), # Lucina - LocationData("Underwater Distro Connection", "Underwater: Gamma Bros Luigi", 8680140), # Lucina - - LocationData("Deep Caves", "Deep Caves: Little Bro", 8680141), - LocationData("Deep Caves", "Deep Caves: Big Sis", 8680142), - LocationData("Omega Nest", "Omega Nest: SA-X Queen Lucina", 8680143), - LocationData("Omega Nest", "Omega Nest: Epsilon", 8680144), - LocationData("Omega Nest", "Omega Nest: Druid", 8680145), - ) + LocationData("First Alpha", "The Forgotten Alpha", 8680100), + + LocationData("Golden Temple", "Golden Temple: Metroid above Spider Ball", 8680101, logic.AM2R_can_spider), + LocationData("Golden Temple Nest", "Golden Temple Nest: Moe", 8680102, logic.AM2R_can_bomb), # Loj + LocationData("Golden Temple Nest", "Golden Temple Nest: Larry", 8680103, logic.AM2R_can_bomb), # Loj + LocationData("Golden Temple Nest", "Golden Temple Nest: Curly", 8680104, logic.AM2R_can_bomb), # Loj + + LocationData("Main Caves", "Main Caves: Freddy Fazbear", 8680105), # Epsilon + LocationData("Hydro Station", "Hydro Station: Turbine Terror", 8680106), # Xander + LocationData("Hydro Station", "Hydro Station: The Lookout", 8680107, logic.AM2R_can_schmove), # Xander + LocationData("Hydro Station", "Hydro Station: Recent Guardian", 8680108), # ANX + + LocationData("Hydro Nest", "Hydro Nest: Spiderman Decent", 8680109), + LocationData("Hydro Nest", "Hydro Nest: Carnage Awful", 8680110), + LocationData("Hydro Nest", "Hydro Nest: Venom Awesome", 8680111), + LocationData("Hydro Nest", "Hydro Nest: Something More Something Awesome", 8680112), + + LocationData("Industrial Complex Nest", "Industrial Nest: Mimolette", 8680113, lambda state: state.has("Speed Booster", player) or state.has("Super Missile", player)), + LocationData("Industrial Complex Nest", "Industrial Nest: The Big Cheese", 8680114, lambda state: state.has("Speed Booster", player) or state.has("Super Missile", player)), + LocationData("Industrial Complex Nest", "Industrial Nest: Mohwir", 8680115, lambda state: logic.AM2R_can_bomb(state) and (state.has("Speed Booster", player) or state.has("Super Missile", player))), + LocationData("Industrial Complex Nest", "Industrial Nest: Chirn", 8680116, lambda state: logic.AM2R_can_bomb(state) and (state.has("Speed Booster", player) or state.has("Super Missile", player))), + LocationData("Industrial Complex Nest", "Industrial Nest: BHHarbinger", 8680117, lambda state: logic.AM2R_can_bomb(state) and (state.has("Speed Booster", player) or state.has("Super Missile", player))), + LocationData("Industrial Complex Nest", "Industrial Nest: The Abyssal Creature", 8680118, lambda state: logic.AM2R_can_bomb(state) and state.has("Spider Ball", player) and (state.has("Speed Booster", player) or state.has("Super Missile", player))), + + LocationData("Pre Industrial Complex", "Industrial Complex: Sisyphus", 8680119, logic.AM2R_can_spider), # Mimo + LocationData("Pre Industrial Complex", "Industrial Complex: And then there\'s this Asshole", 8680120, logic.AM2R_can_spider), # ANX + + LocationData("Industrial Complex", "Inside Industrial: Guardian of Doom Treadmill", 8680121, lambda state: state.has("Speed Booster", player) and logic.AM2R_can_bomb(state)), + LocationData("Industrial Complex", "Inside Industrial: Rawsome1234 by the Lava", 8680122, lambda state: state.has("Speed Booster", player) and logic.AM2R_can_bomb(state)), + + LocationData("GFS Thoth", "Dual Alphas: Marco", 8680123), # Epsilon + LocationData("GFS Thoth", "Dual Alphas: Polo", 8680124), # Epsilon + + LocationData("Mines", "Mines: Unga", 8680125, lambda state: state.has("Super Missile", player) and (state.has("Space Jump", player) or state.has("Spider Ball", player))), + LocationData("Mines", "Mines: Gunga", 8680126, lambda state: state.has("Super Missile", player) and (state.has("Space Jump", player) or state.has("Spider Ball", player))), + + LocationData("The Tower", "The Tower: Patricia", 8680127, logic.AM2R_can_fly), # Mahan + LocationData("The Tower", "The Tower: Variable \"GUH\"", 8680128, logic.AM2R_can_fly), # ANX + LocationData("The Tower", "Ruler of The Tower: Slagathor", 8680129, logic.AM2R_can_schmove), # Rawsome + LocationData("The Tower", "The Tower: Anakin", 8680130, logic.AM2R_can_bomb), # Xander + LocationData("The Tower", "The Tower: Mr.Sandman", 8680131, logic.AM2R_can_fly), # Xander + LocationData("The Tower", "The Tower: Xander", 8680132, lambda state: state.has("Space Jump", player)), + + LocationData("EMP", "EMP: Sir Zeta Commander of the Alpha Squadron", 8680133, logic.AM2R_can_bomb), # Lucina + + LocationData("Pipe Hell R", "Alpha Squadron: Timmy", 8680134), # Lucina + LocationData("Pipe Hell R", "Alpha Squadron: Tommy", 8680135), # Lucina + LocationData("Pipe Hell R", "Alpha Squadron: Terry", 8680136), # Lucina + LocationData("Pipe Hell R", "Alpha Squadron: Telly", 8680137), # Lucina + LocationData("Pipe Hell R", "Alpha Squadron: Martin", 8680138), + + LocationData("Underwater Distro Connection", "Underwater: Gamma Bros Mario", 8680139), # Lucina + LocationData("Underwater Distro Connection", "Underwater: Gamma Bros Luigi", 8680140), # Lucina + + LocationData("Deep Caves", "Deep Caves: Little Bro", 8680141), + LocationData("Deep Caves", "Deep Caves: Big Sis", 8680142), + LocationData("Omega Nest", "Omega Nest: SA-X Queen Lucina", 8680143), + LocationData("Omega Nest", "Omega Nest: Epsilon", 8680144), + LocationData("Omega Nest", "Omega Nest: Druid", 8680145), + ] return tuple(location_table) From b009cfd80fb8774b31abb217748d5db1733332b4 Mon Sep 17 00:00:00 2001 From: Ehseezed Date: Sun, 5 Nov 2023 00:48:28 -0500 Subject: [PATCH 008/282] Lojic fix for unexpected grav --- worlds/am2r/locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/am2r/locations.py b/worlds/am2r/locations.py index 57d75655d61c..617854c96a14 100644 --- a/worlds/am2r/locations.py +++ b/worlds/am2r/locations.py @@ -122,7 +122,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int]): LocationData("Ice Beam", "Serris: Ice Beam", 8680085, lambda state: state.has("Ice Beam", player) and (state.has("Super Missile", player) or state.has("Speed Booster", player))), # speed / Supers - LocationData("Deep Caves", "Deep Caves: Ball Spark", 8680086, logic.AM2R_has_ballspark), + LocationData("Deep Caves", "Deep Caves: Ball Spark", 8680086, lambda state: state.has("Gravity Suit", player) and logic.AM2R_has_ballspark(state)), LocationData("Deep Caves", "Deep Caves: Behind the Bomb Block", 8680087, logic.AM2R_can_bomb), LocationData("Deep Caves", "Deep Caves: After Omega", 8680088), From ae5b546aa4857c969736bf585f573fcf6b9d426c Mon Sep 17 00:00:00 2001 From: Ehseezed Date: Mon, 6 Nov 2023 23:34:17 -0600 Subject: [PATCH 009/282] Mahan wanted the oklahoma room metroid --- worlds/am2r/locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/am2r/locations.py b/worlds/am2r/locations.py index 617854c96a14..8cdaf494185f 100644 --- a/worlds/am2r/locations.py +++ b/worlds/am2r/locations.py @@ -141,7 +141,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int]): LocationData("Hydro Station", "Hydro Station: The Lookout", 8680107, logic.AM2R_can_schmove), # Xander LocationData("Hydro Station", "Hydro Station: Recent Guardian", 8680108), # ANX - LocationData("Hydro Nest", "Hydro Nest: Spiderman Decent", 8680109), + LocationData("Hydro Nest", "Hydro Nest: EnderMahan", 8680109), LocationData("Hydro Nest", "Hydro Nest: Carnage Awful", 8680110), LocationData("Hydro Nest", "Hydro Nest: Venom Awesome", 8680111), LocationData("Hydro Nest", "Hydro Nest: Something More Something Awesome", 8680112), From a2aa7be1276b83f6aebfcfc444f9d8a70d5d68c6 Mon Sep 17 00:00:00 2001 From: Ehseezed Date: Mon, 6 Nov 2023 23:34:17 -0600 Subject: [PATCH 010/282] Mahan wanted the oklahoma room metroid --- worlds/am2r/__init__.py | 2 +- worlds/am2r/locations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/am2r/__init__.py b/worlds/am2r/__init__.py index 489cb5d426ca..cefae7b1ec86 100644 --- a/worlds/am2r/__init__.py +++ b/worlds/am2r/__init__.py @@ -61,7 +61,7 @@ def create_items(self) -> None: self.multiworld.get_location("Hydro Station: Turbine Terror", self.player).place_locked_item(self.create_item("Metroid")) self.multiworld.get_location("Hydro Station: The Lookout", self.player).place_locked_item(self.create_item("Metroid")) self.multiworld.get_location("Hydro Station: Recent Guardian", self.player).place_locked_item(self.create_item("Metroid")) - self.multiworld.get_location("Hydro Nest: Spiderman Decent", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Hydro Nest: EnderMahan", self.player).place_locked_item(self.create_item("Metroid")) self.multiworld.get_location("Hydro Nest: Carnage Awful", self.player).place_locked_item(self.create_item("Metroid")) self.multiworld.get_location("Hydro Nest: Venom Awesome", self.player).place_locked_item(self.create_item("Metroid")) self.multiworld.get_location("Hydro Nest: Something More Something Awesome",self.player).place_locked_item(self.create_item("Metroid")) diff --git a/worlds/am2r/locations.py b/worlds/am2r/locations.py index 617854c96a14..8cdaf494185f 100644 --- a/worlds/am2r/locations.py +++ b/worlds/am2r/locations.py @@ -141,7 +141,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int]): LocationData("Hydro Station", "Hydro Station: The Lookout", 8680107, logic.AM2R_can_schmove), # Xander LocationData("Hydro Station", "Hydro Station: Recent Guardian", 8680108), # ANX - LocationData("Hydro Nest", "Hydro Nest: Spiderman Decent", 8680109), + LocationData("Hydro Nest", "Hydro Nest: EnderMahan", 8680109), LocationData("Hydro Nest", "Hydro Nest: Carnage Awful", 8680110), LocationData("Hydro Nest", "Hydro Nest: Venom Awesome", 8680111), LocationData("Hydro Nest", "Hydro Nest: Something More Something Awesome", 8680112), From de190a5fc2b19a30040f87270cad2a5f6d0fdbca Mon Sep 17 00:00:00 2001 From: DodoBirb Date: Wed, 8 Nov 2023 15:52:26 +1100 Subject: [PATCH 011/282] Added Json reading/writing to client --- AM2RClient.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/AM2RClient.py b/AM2RClient.py index 7cb6f813ff7c..0fab420eb5e1 100644 --- a/AM2RClient.py +++ b/AM2RClient.py @@ -63,23 +63,30 @@ class AM2RManager(GameManager): self.ui = AM2RManager(self) self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + def on_package(self, cmd: str, args: dict): + pass + +def get_payload(ctx: AM2RContext): + testlist = [800, 500, 300] + return json.dumps({ + 'items': testlist + }) + async def am2r_sync_task(ctx: AM2RContext): logger.info("Starting AM2R connector, use /am2r for status information.") while not ctx.exit_event.is_set(): error_status = None if ctx.am2r_streams: (reader, writer) = ctx.am2r_streams - msg = "HelloWorld".encode() + msg = get_payload(ctx).encode() writer.write(msg) writer.write(b'\n') try: - print("Hi") await asyncio.wait_for(writer.drain(), timeout=1.5) - print("Ho") try: data = await asyncio.wait_for(reader.readline(), timeout=5) - data_decoded = data.decode() - logger.info(data_decoded) + data_decoded = json.loads(data.decode()) + logger.info(data_decoded["Name"]) except asyncio.TimeoutError: logger.debug("Read Timed Out, Reconnecting") error_status = CONNECTION_TIMING_OUT_STATUS @@ -113,7 +120,7 @@ async def am2r_sync_task(ctx: AM2RContext): else: try: logger.debug("Attempting to connect to AM2R") - ctx.am2r_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 64197), timeout=10) + ctx.am2r_streams = await asyncio.wait_for(asyncio.open_connection("127.0.0.1", 64197), timeout=10) ctx.am2r_status = CONNECTION_TENTATIVE_STATUS except TimeoutError: logger.debug("Connection Timed Out, Trying Again") From c3084e8f79861fb7b8aaf3a2c8eacba4bf2d7e88 Mon Sep 17 00:00:00 2001 From: Ehseezed Date: Sat, 2 Dec 2023 00:03:01 -0600 Subject: [PATCH 012/282] I swaped 2 brances on my computer somehow? This is an attempt to fix that --- worlds/am2r/__init__.py | 2 +- worlds/am2r/locations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/am2r/__init__.py b/worlds/am2r/__init__.py index cefae7b1ec86..489cb5d426ca 100644 --- a/worlds/am2r/__init__.py +++ b/worlds/am2r/__init__.py @@ -61,7 +61,7 @@ def create_items(self) -> None: self.multiworld.get_location("Hydro Station: Turbine Terror", self.player).place_locked_item(self.create_item("Metroid")) self.multiworld.get_location("Hydro Station: The Lookout", self.player).place_locked_item(self.create_item("Metroid")) self.multiworld.get_location("Hydro Station: Recent Guardian", self.player).place_locked_item(self.create_item("Metroid")) - self.multiworld.get_location("Hydro Nest: EnderMahan", self.player).place_locked_item(self.create_item("Metroid")) + self.multiworld.get_location("Hydro Nest: Spiderman Decent", self.player).place_locked_item(self.create_item("Metroid")) self.multiworld.get_location("Hydro Nest: Carnage Awful", self.player).place_locked_item(self.create_item("Metroid")) self.multiworld.get_location("Hydro Nest: Venom Awesome", self.player).place_locked_item(self.create_item("Metroid")) self.multiworld.get_location("Hydro Nest: Something More Something Awesome",self.player).place_locked_item(self.create_item("Metroid")) diff --git a/worlds/am2r/locations.py b/worlds/am2r/locations.py index 8cdaf494185f..617854c96a14 100644 --- a/worlds/am2r/locations.py +++ b/worlds/am2r/locations.py @@ -141,7 +141,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int]): LocationData("Hydro Station", "Hydro Station: The Lookout", 8680107, logic.AM2R_can_schmove), # Xander LocationData("Hydro Station", "Hydro Station: Recent Guardian", 8680108), # ANX - LocationData("Hydro Nest", "Hydro Nest: EnderMahan", 8680109), + LocationData("Hydro Nest", "Hydro Nest: Spiderman Decent", 8680109), LocationData("Hydro Nest", "Hydro Nest: Carnage Awful", 8680110), LocationData("Hydro Nest", "Hydro Nest: Venom Awesome", 8680111), LocationData("Hydro Nest", "Hydro Nest: Something More Something Awesome", 8680112), From 9351fb45caab39a76e2773258a0816ce59aeb9a7 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Fri, 8 Dec 2023 01:17:12 -0500 Subject: [PATCH 013/282] SA2B: Fix KeyError on Unexpected Characters in Slot Names (#2571) There were no safeguards on characters being used as keys into a conversion dict. Now there are. --- worlds/sa2b/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index 4ee03dce9dc0..7d77aebc4caf 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -619,7 +619,7 @@ def generate_chao_name_data(self) -> typing.Dict[int, int]: for name in name_list_base: for char_idx in range(7): if char_idx < len(name): - name_list_s.append(chao_name_conversion[name[char_idx]]) + name_list_s.append(chao_name_conversion.get(name[char_idx], 0x5F)) else: name_list_s.append(0x00) From a9a6c72d2c909a502c76d5b114ca6b0cf353bd6b Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Fri, 8 Dec 2023 16:39:24 -0500 Subject: [PATCH 014/282] KH2: Fix events in datapackage (#2576) --- worlds/kh2/Regions.py | 3 +-- worlds/kh2/__init__.py | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/worlds/kh2/Regions.py b/worlds/kh2/Regions.py index aceab97f37ce..6dd8313107fe 100644 --- a/worlds/kh2/Regions.py +++ b/worlds/kh2/Regions.py @@ -1020,10 +1020,9 @@ def create_regions(self): multiworld.regions += [create_region(multiworld, player, active_locations, region, locations) for region, locations in KH2REGIONS.items()] # fill the event locations with events - multiworld.worlds[player].item_name_to_id.update({event_name: None for event_name in Events_Table}) for location, item in event_location_to_item.items(): multiworld.get_location(location, player).place_locked_item( - multiworld.worlds[player].create_item(item)) + multiworld.worlds[player].create_event_item(item)) def connect_regions(self): diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index 69f844f45a68..dd57f5e759d9 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -119,11 +119,15 @@ def create_item(self, name: str) -> Item: item_classification = ItemClassification.useful else: item_classification = ItemClassification.filler - created_item = KH2Item(name, item_classification, self.item_name_to_id[name], self.player) return created_item + def create_event_item(self, name: str) -> Item: + item_classification = ItemClassification.progression + created_item = KH2Item(name, item_classification, None, self.player) + return created_item + def create_items(self) -> None: """ Fills ItemPool and manages schmovement, random growth, visit locking and random starting visit locking. From f10431779b9525644b649c92e67977df03359d80 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sat, 9 Dec 2023 13:33:51 -0500 Subject: [PATCH 015/282] ALTTP: Ensure all Hyrule Castle keys are local in Standard (#2582) --- worlds/alttp/ItemPool.py | 2 -- worlds/alttp/__init__.py | 17 +++++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 88a2d899fc60..1c3f3e44f72c 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -682,8 +682,6 @@ def place_item(loc, item): key_location = world.random.choice(key_locations) place_item(key_location, "Small Key (Universal)") pool = pool[:-3] - if world.key_drop_shuffle[player]: - pass # pool.extend([item_to_place] * (len(key_drop_data) - 1)) return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, additional_pieces_to_place) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 32667249f225..3f380d0037a2 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -289,12 +289,17 @@ def generate_early(self): self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options) self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options) - if multiworld.mode[player] == 'standard' \ - and multiworld.smallkey_shuffle[player] \ - and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_universal \ - and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_own_dungeons \ - and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_start_with: - self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1 + if multiworld.mode[player] == 'standard': + if multiworld.smallkey_shuffle[player]: + if (multiworld.smallkey_shuffle[player] not in + (smallkey_shuffle.option_universal, smallkey_shuffle.option_own_dungeons, + smallkey_shuffle.option_start_with)): + self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1 + self.multiworld.local_items[self.player].value.add("Small Key (Hyrule Castle)") + self.multiworld.non_local_items[self.player].value.discard("Small Key (Hyrule Castle)") + if multiworld.bigkey_shuffle[player]: + self.multiworld.local_items[self.player].value.add("Big Key (Hyrule Castle)") + self.multiworld.non_local_items[self.player].value.discard("Big Key (Hyrule Castle)") # system for sharing ER layouts self.er_seed = str(multiworld.random.randint(0, 2 ** 64)) From 3214cef6cf64451de1d4d9305f1ac82085f02125 Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Sat, 9 Dec 2023 22:23:40 -0500 Subject: [PATCH 016/282] TLOZ: Fix starting weapon possibly getting overwritten by triforce fragments (#2578) As discovered by this bug report https://discord.com/channels/731205301247803413/1182522267687731220 it's currently possible to accidentally have the starting weapon of a player overwritten by a triforce fragment if TriforceLocations is set to dungeons and StartingPosition is set to dangerous. This fix makes sure to remove the location of a placed starting weapon if said location is in a dungeon from the pool of possible locations that triforce fragments can be placed in this circumstance. --- worlds/tloz/ItemPool.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py index 1d33336172d8..7773accd8d7c 100644 --- a/worlds/tloz/ItemPool.py +++ b/worlds/tloz/ItemPool.py @@ -117,6 +117,9 @@ def get_pool_core(world): else: possible_level_locations = [location for location in standard_level_locations if location not in level_locations[8]] + for location in placed_items.keys(): + if location in possible_level_locations: + possible_level_locations.remove(location) for level in range(1, 9): if world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_vanilla: placed_items[f"Level {level} Triforce"] = fragment From c3184e7b19c65a7ae649d5878ea97e068080967b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 10 Dec 2023 06:10:01 +0100 Subject: [PATCH 017/282] Factorio: fix wrong parent class for FactorioStartItems (#2587) --- worlds/factorio/Options.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 18eee67e036f..b72d57ad9bfe 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -2,7 +2,7 @@ import typing import datetime -from Options import Choice, OptionDict, OptionSet, ItemDict, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ +from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ StartInventoryPool from schema import Schema, Optional, And, Or @@ -207,10 +207,9 @@ class RecipeIngredientsOffset(Range): range_end = 5 -class FactorioStartItems(ItemDict): +class FactorioStartItems(OptionDict): """Mapping of Factorio internal item-name to amount granted on start.""" display_name = "Starting Items" - verify_item_name = False default = {"burner-mining-drill": 19, "stone-furnace": 19} From b0a09f67f4f38fac15f90f57e4b9bae78e37f357 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sat, 9 Dec 2023 21:43:17 -0800 Subject: [PATCH 018/282] Core: some typing and documentation in BaseClasses.py (#2589) --- BaseClasses.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 7965eb8b0d0d..c0a77708c016 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -491,7 +491,7 @@ def has_beaten_game(self, state: CollectionState, player: Optional[int] = None) else: return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1))) - def can_beat_game(self, starting_state: Optional[CollectionState] = None): + def can_beat_game(self, starting_state: Optional[CollectionState] = None) -> bool: if starting_state: if self.has_beaten_game(starting_state): return True @@ -504,7 +504,7 @@ def can_beat_game(self, starting_state: Optional[CollectionState] = None): and location.item.advancement and location not in state.locations_checked} while prog_locations: - sphere = set() + sphere: Set[Location] = set() # build up spheres of collection radius. # Everything in each sphere is independent from each other in dependencies and only depends on lower spheres for location in prog_locations: @@ -524,12 +524,19 @@ def can_beat_game(self, starting_state: Optional[CollectionState] = None): return False - def get_spheres(self): + def get_spheres(self) -> Iterator[Set[Location]]: + """ + yields a set of locations for each logical sphere + + If there are unreachable locations, the last sphere of reachable + locations is followed by an empty set, and then a set of all of the + unreachable locations. + """ state = CollectionState(self) locations = set(self.get_filled_locations()) while locations: - sphere = set() + sphere: Set[Location] = set() for location in locations: if location.can_reach(state): From 6b0eb7da7989bd7e1516118bcac84b1cc0984452 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Sun, 10 Dec 2023 12:58:52 -0500 Subject: [PATCH 019/282] KH2: RC1 Bug Fixes (#2530) Changes the finished_game to new variable so now it only checks the game's memory and if it has sent the finished flag before Fixed ag2 not requiring 1 of each black magic Fix hitlist if you exclude summon level 7 and have summon levels option turned off --- worlds/kh2/Client.py | 5 +++-- worlds/kh2/Rules.py | 2 +- worlds/kh2/__init__.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index be85dc6907be..a5be06c7fb16 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -29,6 +29,7 @@ def __init__(self, server_address, password): self.kh2_local_items = None self.growthlevel = None self.kh2connected = False + self.kh2_finished_game = False self.serverconneced = False self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()} self.location_name_to_data = {name: data for name, data, in all_locations.items()} @@ -833,9 +834,9 @@ async def kh2_watcher(ctx: KH2Context): await asyncio.create_task(ctx.verifyItems()) await asyncio.create_task(ctx.verifyLevel()) message = [{"cmd": 'LocationChecks', "locations": ctx.sending}] - if finishedGame(ctx, message): + if finishedGame(ctx, message) and not ctx.kh2_finished_game: await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) - ctx.finished_game = True + ctx.kh2_finished_game = True await ctx.send_msgs(message) elif not ctx.kh2connected and ctx.serverconneced: logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.") diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index 18375231a5a6..41207c6cb3d0 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -224,7 +224,7 @@ def __init__(self, kh2world: KH2World) -> None: RegionName.Pl2: lambda state: self.pl_unlocked(state, 2), RegionName.Ag: lambda state: self.ag_unlocked(state, 1), - RegionName.Ag2: lambda state: self.ag_unlocked(state, 2), + RegionName.Ag2: lambda state: self.ag_unlocked(state, 2) and self.kh2_has_all([ItemName.FireElement,ItemName.BlizzardElement,ItemName.ThunderElement],state), RegionName.Bc: lambda state: self.bc_unlocked(state, 1), RegionName.Bc2: lambda state: self.bc_unlocked(state, 2), diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index dd57f5e759d9..2bddbd5ec30e 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -465,7 +465,7 @@ def hitlist_verify(self): if location in self.random_super_boss_list: self.random_super_boss_list.remove(location) - if not self.options.SummonLevelLocationToggle: + if not self.options.SummonLevelLocationToggle and LocationName.Summonlvl7 in self.random_super_boss_list: self.random_super_boss_list.remove(LocationName.Summonlvl7) # Testing if the player has the right amount of Bounties for Completion. From 6cd5abdc117328fb327168bae2d43a379b64e8f5 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sun, 10 Dec 2023 13:07:56 -0500 Subject: [PATCH 020/282] SMZ3: KeyTH check fix (#2574) --- worlds/smz3/TotalSMZ3/Patch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/smz3/TotalSMZ3/Patch.py b/worlds/smz3/TotalSMZ3/Patch.py index 049b200c46b0..c137442d9bd0 100644 --- a/worlds/smz3/TotalSMZ3/Patch.py +++ b/worlds/smz3/TotalSMZ3/Patch.py @@ -319,7 +319,7 @@ def GetSMItemPLM(location:Location): def WriteZ3Locations(self, locations: List[Location]): for location in locations: if (location.Type == LocationType.HeraStandingKey): - self.patches.append((Snes(0x9E3BB), [0xE4] if location.APLocation.item.game == "SMZ3" and location.APLocation.item.item.Type == ItemType.KeyTH else [0xEB])) + self.patches.append((Snes(0x9E3BB), [0xEB])) elif (location.Type in [LocationType.Pedestal, LocationType.Ether, LocationType.Bombos]): text = Texts.ItemTextbox(location.APLocation.item.item if location.APLocation.item.game == "SMZ3" else Item(ItemType.Something)) if (location.Type == LocationType.Pedestal): From 1312884fa2deb33546a500fe743e371492b67806 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sun, 10 Dec 2023 13:10:09 -0500 Subject: [PATCH 021/282] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Fix=20Silph=20Co?= =?UTF-8?q?=206F=20Hostage=20(#2524)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes an issue where the Silph Co 6F hostage check becomes unavailable if Giovanni has been defeated on 11F. This is due to the NPC having separate scripts depending on whether Giovanni was defeated. The code for the check has been moved to before the branch. --- worlds/pokemon_rb/basepatch_blue.bsdiff4 | Bin 45946 -> 45872 bytes worlds/pokemon_rb/basepatch_red.bsdiff4 | Bin 45892 -> 45839 bytes worlds/pokemon_rb/rom_addresses.py | 12 ++++++------ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 index bee5a8d2f4996ee1db54d2b7812938cc66dcb922..5ccf4e9bbaf80f0167471e02dd64384b5bb0e622 100644 GIT binary patch literal 45872 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 AWB>pF literal 45946 zcmZsCbyOTa@aLjk7I&w3aak6Z;<~s)aa-ISTHM{;-5m z7_LyA3T!@C$T7zW=~JeU3@Xo36oxL|FWxJ1a(+stkEPF+^nocejK|UgXVXTp<(fI$FY^=6+^T4v59cUd5HxJ&;S@19XS@A1Z++W@X0Hd z5FHvbN6ti+js=h)$7CUg#jDtStyIY?EM9~=g`ktb6#$6;XExx96-fXv0CE5cU>0F< z7BKsdI*X3}kAy&(NE-{OJc$^{D+@fuR9TRi7yiflp#lN}K+r2-=wJXuSd^jw;FAf4 z{A-1&@-H%n`GiRapoLdB%*@vqtV0dqky1^V?B;8-@f)0Da|wp~qjK!+pr{6Aniy53 zYkR6p(=5PlZqqt$lcFUq+sJii8f+t6U|*Qw6*eNye)Yc2sEq2~H##?+%=KS75yubq zQ$dspaqWqKG3FE=DPmKcg--npZ-0Uwjg`xk<26NEHYjO|5gg_DXwql+?cy}~&uDeh zM6tc!btuQ-XxM;T;-P5b7&_EI&&MHFY%dl=Th7+Q5xUCxj#d-iS(`STx7MrJ{iuD- zG!qUhs`T@T`%+9R@2+a z8wA!%(0Kh`rPlsr&Xg@_yQ^PvAu1(l2v8G95C{MSafl8?V4?99u7?0XE_@WFSp_;m z3b-r^Rbu!+kW-TEuS*tLU;jZJBjyd_jhQ(2x<8ZvHh5$cB4c)7-xmreF%c}Jxb9(I z0?U`YQrxR#UY{0J%uuV6J6-*{PT!Ssz5HGbKA1Rw;UBk*8}rQ=@fIzHDQPF_W!gjS zqvOw=cItJ6QocukF&-Y*7~5dLn@J`GT$FFMy4XKnbXJ@NGBaK@lMpG88*x24LH13(Qo&>6u^ECWwG&8&|1~8n=FD%Oh(5vI=wKbM zTt49sWC26lVp1V1B(2ScYroxAn@gE4W=^JBBDf`3PzMabuWv?dk9^F=w?2jg!28;P(E}qn2u@)x=#% zf{Kp51vk}2gsP*snw+I-EL|{2L@cI=@S>?46|=@^Q8sqAnqih~%;-=_9dc%Dddga` zy~8grw}7LI{@_ zf05l{Ys(u5ev~v9nz3$~Sz77Y`;MM8l-{X5A%t1e_mvqIb!2Y9Q?Z3} ziKLKMV4cJeDzDujSnU&H)RB!kH=c)j=*_gH9X=AfedJY^F~~9LL+131*Aa7n9Wr+0 zvaECr3jV~L9S>Ldj?ZA#8|BR9c$K+p>#@#Sr}Yh%@{mf-=4&oF^~v+Rsz$b?CUg%> zuqAiRy3mKKxpJbM@^^%NPclhVUwRxMyf{=KO?$7Zii;CnVO17SQhD zevD%ofFzs$t?CEMF}(ZT6Vq*Gto({LX{q<5?Xtb)Xc>2f-&Y^Sv`6HT>W^w`XDIw|=R4Q|#}11T@E z^}OTx$}jT}BUse56oEQ$&W9(YG_eo)v6@Jo73aVyixfR}Y~mCRz6_gLk8kBCpl zZCl!sX$^k}%FvnVQ_HTZoX;|N^j%T7>mONpmB!PW@jY;si{^PMy9klOX)xkF8lsPg zDs23`xDCL#1Vc>S=pF26Xnh9)bDX_5krut z054LFd3C20jnJX`p-4>QmH~+N~D$}W6CZ+bAAxl3DQCle~X$EKJaTz%;d)&r5b8dB36re*T`trD}>8tbXST<_)#q4YM#-+fWUXb~a zC?ysOpbriOh)4ww2QQk2UTe&Xb` zM(gmRw0I#vR045bH1>7A=Zx!%_$na3vi@l$j(@t=kS#LMCN8)di!~9OqQ@ zsJ2Qbs4YvDvk;MS+ODDAa1bCnGAg_6d?ysP{cezF$lOPTRnR{?mKZP+2!x~Sb`q+W ztMdQL!`n8w^xb|l^~dMYg6}x8$yi9iSb+^F>!`?y=m1JPEIZ%$@T^&DpP0`i8cjkN z>bz~Xd>40J?d|{7?;0F?s6KX}4f&p3Lh}L=sDg-q#{B_k$n>94c)5_!L?!r?_4N|V zDY0-HfpBVCgs%nDIk{)H^#eJFV2dL$u{W0D|{k6!D57P*5Qr{*+ zn7VK)J_G>RJ^j}eTJYN=yA^mb3N9iv)ZZb0cr9JffQSG}IG_X^0*;wv#w7+jE+#29 zM~iT!;GKEho@u?ZFp<*m)}=W5LOQ!F9Hm^XZDA}?9u^N9y&S7|756~Eg}U&jZ+;ua zKV9Zb(-!MG64Q`uUpC(Y3kz%jYiEC|kD59T`AraU>(FjQFb{r({Pr&u9htODHa$MO zNnH?R$Xm9n_#=oiC!p8>P{K-7_oP;(Ek{R}Pn)An>TiprO->QxB1F$Fmqi3wjwCM- zZ~V~j>j}y^w~IjgeSkQ`M{kK3NGT~Htg$67ixWywMu=jrs#J`uJ%KE@luDIMnVIgu z6v6?zqzA~NaLDnbqJtje+DdiT%`u2F3WugKWD&2lGqi933jPCHK4tJo;Q16u7s-{H z)JP&)RiGoA_nj1IVj-4^S{A0pfWnByOUtt>z`bhRd|Wvk_s!3-I5Wf1rV z>X#k*!=W-LtWwSVsy9)4rR}~@6*t`1uW9bcQOFn(EGiD?Gj9QzwAND=9%#r2){=g< z0%1`acrH!io^7y4aYqt3Gif2p7N1$}vda;W5QEx{&x0n z18v6ZS8lrM!f(bB+v3hpGfhj!VG!l=C8?m9?o>u9@)LtY;Mo>azB=p4!T@}x6x_6} z)ccKIYb9+%m)SL?;i^BTOs#-i5d&GK-g;_9v}&qk$JcAnY92%uB!Y#@mm>vEqT+Tk zOS@9K4U!;yLMo1c;gS()c=JR_h6lGQt$=*nn*6Qk=tyHqM2FtnCF}vzwe%S!*5_Pj zB?1%4B7S??i6U;2QL4-qOB%a-?^J~doT>UnsGCk}fy`#cryxMjUux)=;_8v3L%Gtx z%ZVm4LEn1a{rfMFKhhN@wzJacDGe&-c@o6#Z{}>LxHS?#Y9>~uBEh3vkTT*m1U{{B^!rDxDJXmD{*s6y(VSPa5ksTV1#1L9uVTrg0oG zcL59$Jv+}oTfZoOwg@c2Z38FO45rXY+Q{*}PgjGEOr?<}#I>lH`ME>Al?iisd;I$S z+R^XA?;gB{vCzg87~NA#+iG9D49nhAogJRQ1ii(zTJwLTP+*DFF|v!UM#?4%ipypk zEzeUkj)G-r?ER~zr7md`YP&20A)TCwWFzs6jip29|1FMCKkI1|A0ae5S2v^Pj<>Ro z?9X!Z^Ti54wtNjZ8jiW-erQ~p`uXZ+g7lH5rV>izcr} z|8_4m9DoW<0v1+8h7yQ@YbFsyv_omLAIe}4Z(qm*K(j8}kTA}nLh#6>Od_#v+k9q+ zz4+F4s`nRNs5Y*iS-ub{<11&l3sW}Cw5%pct?}aO`Q)O0Vyvd#J7QI{Hz>FM+kWOL ze24xtr$-yhU$>D1Yqz$A`3}&=g&QtkD3JGbr&-n5tIX9Br4EnE+g8y~c^5Jfo}O&( z2j@*jZ2qRMxlXHe&Es(rp&!JcNPE3!jxVTdP;4)eGCTdthS`XPfV6Cbwc8 zmmk<-<694}+WO>7T`**B9R{CxlN;B7N$T2@Y!9^j@nq{oQsmkU?x)Ny(gJiF7594ZW+!w z&Z#mbNZ{KkQ(F>kwozZ&xUF7x5eJMl6Iui%hSx@crsmWI*;@B2oFlQL|6Krrv+?m* z&3_jaUX$ZJ(vg`8^enK=6lVAnJo>Rz!yu@)iD653T{E~J-wej_cEu&ccz6!}ABM6e zwKob#3HkpZDF2(Z{eNqA^!PRCo=jEBYmPm+MYY=s=%he+0ET~WcJ!Qk8yzGSx_{XM z5IQVEUi2A&5dh!bBljziU={9`N|K=fpaNjP0RTV%#-~2Nm%f*?S;YN|I?Mu8Cx?=T z=RS{xB%jF(!>jUC(d@N_nk#wNA6FjpjlU)reFYQJKXC@twSLGb{!n+@?O`m~v$iV| z=Ge4flyY)h>j^1Kv9ys#CRB+%1o^z z#>;3Ql$AktA=wb?g5>+eZgF3E^9SN6Z<=Yjwv-2oe^^UVUU`yGnM_%hHD|>-r71;X zw{*l*!QucQfD#BmDY8ttwT?_#htbRXeyXhcT8wL+m#iognc~fjO!>7kuVS^l0#h!5 zqIeiUQ5INWeMlly0D~+{Sf3(?m%tSu03oxIhzsHn68bFY;z*fId9xxFPb^q{TJEGW z`93RK{Q;6qRb>2c6zUT`io(*|ko!FKMF^>A=0BtaHUxn{U?gZ+5E#T9Y%j4mmnhpPXWhuf{K8mNo ziV%Bx(Xm-!WyV7Q?4Q^bYl{Eo4mkk*Lj_>GAW;riPAIHcdUlaWSTxH_#k#z#42(Mf zv4%TPgkaLbgo>5bX{gG=MCZz^3-ZchiNP?K`Rv#~EDKi@VIE8ZnO#ss&f-K4qD_z- z!v)ALJ_wgZo`MxI8SbUy#q)|4i@an?vUrlY(b2Ni>1nWp#S+Yg|KUut=E5|KkXfGq zVgzYX0105OEHJTn4(=n(Lt1@0eW^ywRrmp)-g;kvD(Y3PjQgt_*oziA>tyP<`&s&4 zu_zLpOubEIN0hI(G^YU4<#<3n)JpXhiaR>?djTsciNU)&)S$B(UJ{5*c{}GU z$@hKSZxJ6X0us{p%Zy;fKX>{$8V_^k{Kt3en}J0e5vtyJBQySFs0PcI-=@w=KfZ*N zlImlBfe z+zXv1F96%E6*#yz$QeZ9xkfyy~bYFztxpGQnNZCfHbxD{|qO_u%e-*^(?A5bo@sqN1KIz|asySR|;W@WJ z$-tWNhtCMPgB6P%9Pia;=^vTBA$7wE*_};3=W@&peIw$~>-lf6zxGL^|AAM}|ez|xHsJ4ut|T5@xITh-xZ45DujpacEihh{A$x_1nenI5J2 z2wHn~Ru!{n^ajD&ImhdTgQ}M$MNy3A!qpcaT#>5_f@)VTxl=o$gy}9z)j_|}b(OZA z>>4r`DQBjXVVPCMoJO3{gc zg~hx&2Fg%|ETs-vJh_I)V+1)4rLwk!ra=(S*Gx;wflk>BvRV~jMo>U#y^BUTO2)5G zY2WGP{_6Y)pg)G-@N$bIASMTMX{~e?ddpbjYZwlwXK~G>aGQveN9xUpjpCUBBZA{P zxF&-BE-b*j;a3q8g5$Ra>+CI;RqDFtb&zPh_@@gXF@eB2+P_KXc%n~Sce8>ff<2kW zuWV|`p&0Oq3U05a?Y;Q-ogE%JU)(=n2vmJ&0p5Q`h__?mjc`r?UT!+jv+m3n>^wRo zN3M{oqE+;VC8Z7>R{@T_{bEy!@ec)_t)ZdMb03DkO3c4_i|58JI1J@XJ$*za4%f*O zzQI_b%VkHlBl7thq}a~=gJ|NfVnDA`&BObMrdTV%qFyznth79)6e+op_Z?$l3x(>_ z=M96kKA%HSG#%k3Gv%)e9Kpe^paTTeUuyp(omDUC6+{n+NW9nSirp-(Hn^1Xablz` zs71Zi=RXRDI39D;D}VH#ocx;B*V?M&fLbp|Ep7~dBE_{_nlQHAm0V;9jMB?=89*p7 zH*KY<9?dDt@7~cvHmnuT9aiBy6!Z`O^wNoyyI4=XUfFHgdid+$+^qVV_nmOzX`4BVR4e!U z2UTYh^mNF^@WM1p;1GLeV-O-8pWA85=6g0iKqfsTlYCw#X%hB|E4UdbFz?8HaVqVMeBlTQGN`eT`JuCfG%u+&v^ z>%@6eqnX%6QDr9@SW?iVVbr78S}yDuoupO4G`dwhcYv~+Sq}KwDS{NMMwz}*ApJx7 zO}IcfCb0$o@h=_y%$>fPXP{(IICippf&Q1I9jtRcH|qSXSL@9Sq6BKSM7RX!M*wW@ zcaQJ{?};!eV)hTglu)UW;MC>zG666JvG({UTAeH49tgS~Sk3xw^55^XFQBkK<4mf+ zEMh9Sw4J}b$6`i=J}i%>4yS^o*Ing&h1m-~+<{=k0ZQ#p5;Ai0iHVtsw+f~@zZ&g) zx}C7*$$Kh#Pn0kYOXNq0KB0Q$*ruQ7++>$|dz^}{<~orO5gYS<49j7zW3$gy)L7Tn z&f-vCIr~LG?b_%`>V(fQE&Y%xa`ic4yZM)Q@-SmZtEHxaTG{4fpov&KcjX0IODc8x zD!a5{0yP}pQaDy%qYsWW6{fhQBo>y?NCJ-$E_fiPVj@ZQ!TS({TQd<|?JpfqIXO|0 zYOp{|qXRg5>tfy$Q&Dw~DpJCW8u;4ZT^wHtErW6l=cQRoibn>)u^~D_{y0e_%V57S zRbIf|;!+yCFGm-S(fu^IgWogJ#WRzQ14Mx+j+P0CBzy*qaMCl2CXY4-f&GEOsGixl zi79#cwxoZOZRLu1P|#`)&6DugITISPy3EJmT?y-BsrGx~ZF!we#J;1lZPKLE!5<18 zFqdLamsp2G3{OeE>1xOx8u?#OU@Y=!Y)(c>{;_J{Mb*qC`tv|MXj}a)C0?CVzqal$ zphAq>lb4W26QeMZ_NEdiUh3hSjNk6CVi;_h-K{je*Rf%?zBCJ&1l6+pN2WhqT-xLW zXOI4az9maPk&@CuC+V}pW@5owEb7HjB?$9N8tfKjy(wAExsOP^p|Z7JV{?-*s^-gC z`@UOOY%|+pSDU6xy2iPsTxJkHR;F!HSHMmjFqitz5UB^vx0FSg^C-ZbVP00(P}k&i zlIgcqs1d&uTfO(NxmeANbXCD*lP>P?Mm2{Eur7u}>X3#P?F&(yZnhqy;9i>T3PW&9 z%igt2DsJWcNo@BdC-kozf3`S!K*OeW_^`;Ky}cu!0}^`UV71;~cv9(X4;j9q*rg1? zqdfAAj&FpVSqdUAjS6Kc%a+aM^W_Ex62VUAWvuE#G;?ijw|abedh;DxoriwwoC>s( z2Q+*f5qsE_qEm@dV^<#}ve*L{-;-I=YC?4}n(vfI>kAE4iE}8l+YvuQ-io$a4j9xJ z@!v)kPs}PvZ5gYP99I^Selbc738f~pFC@}K|2Rc(l1WiM&~X@62FyB^%K0v}+;x}| zZ@zT5bo=};|D@yk8|zim*7+`n1SPXfhNFrFmlQTedC=xW8k#;a177yH>sjs zt>cr7;DLoOsN2%q*={F7`uRiWeW>qs6`L(a5$n!e^L}6C<3@z3cH;V>4H7lF3#97d z#-%oxUhweC3Ujlu?_Zx@R>dT|jhP%>?t(=qag*^pquDAQtG9%_2FZQ49K7uDwHFYZ zQU0C47QGY4P|30#(~^eZT!F6V$$4Qs?o{b)xIrmgRBxxy8_KXUYxg>{zHkapC)SU)V}6Q;24WdFh5(BZj}Hv60r9@>7-w+h5AP zKq_r)N1{6d^s1#0k}X`3pz_@g0qKuZW0^pMl1^?6H;Q=1Cf_DYi%U99)VE8mWfT@h zJq7I99Ccu@)Z4;;-fbo}cJ;2F9yO;c*;g^iYG+$M2$h`6W5){|mC&r~P*zkVQhQS? zh9^psPS%5-3G5^HIad*?ZFA*$Cu_a+n(t^F^}o=Fh9unP2@_+KLu1qTmRW-xtUA(@ z_1#m~Qt28cKjNePn4a$1r|KFdfeb~LrO^}k%DY>nzD=*?xH8#$+vd5P7nt#MC3j!! z(x`EUIj2-?Ye*SS#=2-V@nrc`mFzw5t#n3>Hf9pqy zucM>GnE~%h^=idmL>bZicROIKXq??ckiAd5*h`A2eMUZ-gHO7TZXNQePQJOgMjg&E z1kV;XY|FnR(^ZMtV)VWndUX#@*~gVp24|F>61?Q;bs~wcX1<%Fbq(wBI#}saG6byT zWQOX#&W@j`0sZCC=hCco4I&F=aSB!A!S<*852W`ia;W6Fq$?OFL+AHCA8y9$TLy6i zT~!!baaY9jBUh!)i|hE#EoxRb|5+x|Y}cnFb1BQ*+_)kg;=Sz9)ZECYiMy2 zh&U0H(&9ss##JE=F~4rajUlf{s#nwr2h8Yw(%{eaPWUe8YAmhu@dvHJO`3}q)%2}c z8FRHUOEsGE_k5RR9?pxe@!}GtiF#Z70woulY2t2E>d}?OAe;-_o%EGKgI)n36{5+e zxT5)mK8n8Pn&?m4VdON|m}um~f(n^{HZs01?1QNs>?G~#DI~IdQyv0Qt>f6gGi3(u z(X1@P!aqwXFWPUOld{*etFq37R$C=lZ@bl|dRosv<9ht?Yks?S;UiXFri|0)3g#>g zY?$A7IsA%k+f;5&?+|L{$*T*-Q43}`6nrIPj=il%!!MHUY$_;FGqL=t05*hmDL{qV z#V#4uKx__k{8g{=t0EeMovNpj$~eB((QwnnZ*HqY%By=@SgFn8bJebb*V$?{He1b6 zqxQEl?7wvs5#38Ykh)kiBz0ykmqmD4Ws%`I!eD}jmVE=e1yzVF%;0=pH<)Qp@l=$RXvgtZ#StKbfUH+3) zk9=JNGdJBbMk+RXmn$5&<`ypV#caC!1}7HDMb0Z0m+=b|xR{vaF~;~oCI-6l+kH2U z&LXX9dE#ZJI;E~*@wv5j^p>eacq=Lm_jjgCVJ#h?*Q%8`+Tw;Tv+?j?byAS40l=Eq zi%R{-!^fH?(XpBEQ@efT8h`odc$S(av{Qb3aURW^vl}g)=gU{Bi!ZODGIK6JPAxRg z*cMF{*N5>~z0lSFnQwP3TC2b8fmfPQ_gkYptSCoNUaZTyK1AOcWqLk>?9v5)R-<0o znr`v#lb9<+Mm zEWhRH&QF5e;^B;aEQLj2o0ysqrvlz^hV43s4EHqeAitxY60e<6i7znSTfiH=HDT2b z;r>8f>+nzfX@T(wPy*RE3Q>EN?dNB2D!$S2l^l8Ss~$NnBqg~xn~np#5_EFDxX_qM zgtTTs&~4x`RGX97z)TW>z$r@CmyMYE)j$V9F$Nwqk9g6r)>EyA5vFGZ@^U!nUp|}j zq@U7Yb`=wTk~%apOUkACwHBR3(-U=ZJ8Omy-&xT{!pVgA&|*?6OYiWL`TDYxJz|!9 zcJ&9^^_{0W&}Li?V*wh1(OD_vu6nJy(9?k5-g2R zR48Cv4=v!IHc+HSqQ#uyq7jW3RZ9vALY8wRmHQ!mOkea~>Yi7gnr&#F)%5+(wh0o% z9D~1juGRqrx28>>DtO~YRfzPW^Un!~Ngt%Riqb_<$LuFMSA~sf1jocgKBRGBS`0(& zhvSmul2oZ2j=e@qkXi}aqF_Plq?^_5jQS+cE4myjB~u=;k4Hg^zZ=0$0N0Nt(#i0z zn1|AeBzd)0nFgRQQQ;Ef*;!|u^VloM>0k#l#lfPi0!~MTR7DjH%b>IPUA)RxKL#4R zls5A8QUK)M`ySx^WLp4x5InMxgkh1(8X#mnnI)VzD8_%9^h&-u3u3ldO`7t@w%Ps5 zGHZLmB;F0=1Wt0~yT>7v2o#t`MFop0Q|7%4>gQ_d6s2T* z89jKiKkhNuGbIszoboS@{RAZhy$TEzyZ2zz{WP6&7ar zR1!F9({I|3B+pK=$&jABbzX!fgdHQh1wIOX6}-(4kQ~7Q@qmr{g?O1xDc6C z$8oFnId1QcX;3CHw7kQwOeOl4jp2H;kHdzVI`3WycE~4Qq4PDZ?-hRS>4FA2q%!ow zSbBjWTv>&9K5V-Sx(#@nNelj}Rq(bNJQHFJmGZR1sTcF7syV8ua<3t*yn6*vV-;gt z_7bJtKQPk#K@5|4RT+vg?D#xp6+LP0v`3H}#AWSXwca`@p)8t6{ zs=BSI8Xv9KE!8z}ee4x++3J!O#e%3S_6A*3<}o;@O)j^v&1abURStKVb#dMagNb?UP64ikhn zD`#qOOptJLl_qB#)lhY1ba#QfZltu<@neV>VuFHN%|>nl|9j=pYhWw^xm6+Qx+}=Z z59_q0+zm_UPD=AScgf@YjVdF3MrTn!>$Zu~b8BWNkQP!4m5m)v`eOqVO)(O9J+{O< zY~|gvf69G0$qeofMQ{G@{!!b0ko3LTuWqy@C9n;RzCRj|2V5+orulQMH@xv*R9q5MOEl$q*^_=HZT>uNx}bGi&GPHMFvP~*Ub0bq`k86dkB5AvTSAAl;Sg` zGi%7*NQu$CD;3>R9HIQeGxiEE_s#~)rdF{ECN>=I1-gyx@$H~Y^|1oR$d#)<;omk+ z$|jPdF*He(+T0#N6h8Sbp3K2?pKOJZ!C*tyyU_<#L;^*Hi$6wd90$_U#}~axo?iO$kPzQjy>GNm z{(S#?QgkIR$DPk3<%eV9z~h%ru$PvVC_WL2AyZ{vuXApxaeV8;zSN8sF5uYK#?qa< zzu&d~PSEj-zQ4Grt;M+nK)Mt1Ta8sr6eBkvh@y(Jgj=zho9UI%w?U_E1UQ=dLqsY} zk-vsE;j%~1FAOW;it=~IN^e!fx)4L5hB=<`tz-8O7w_DfaOQF?@)k~>3rRKSLKxTpT_`#JyuE{3yV-pSe2Oa?coaOb48nWa*}mV*3;UzfThKYsnFNq~bK+ER{}ZBrn=aF+oUDW;15 zS?pM6f*JWb@udiZts<=g9|a*9p*gHZz4=d^m9C46aGS%gUB#JL`^n#n+VWK2A$#Ac zc4blpNbk=c{~E?Vh3TNs5Dn=tQV;XDcN_FlSS5rlBM)n+eJn~S3F3&RdVBwzY_enm z-PI^Q0qbKfnum8NS&8BQ^Xq8QZ}pX0lWihp!mhonrB+>QDrpqRL6a@J>ASZOP7L!-FOZ1EZ$#(2`HfnvqCmm~WM$Yqch=H)udR zzGDsb?sK4}K$VlRykZ|PC%Ww`U_8@-1VPQYE7WmWld64HZ5dFXGFv~Wu;qW zZ81^QO4Cni`CDX@iIdZ?fCMaxC`%QuY^<(XRS!~S$sVq&?dW_`rLq z2#lD3tOz7*TT$hASJ%99`wx^!Jb#^)>(8uzS|}(V`h6m*lW)QBIq=KMm(IE2OvRa+ zy>WTA}U2j3d-v*GhaqEk!luzz~i&F(n#u{eS5&f#4xZ_dZR$ARi!0rM? z=T=?~PrTa_8hGk5EcB6SYfM%4n%=fAq%3{*Kds6IV3%^dF`7{&((wI=1RryM&+Whc z=k5ZVvF}{f#?-GrwIt!Fg=KZ+##NsECni?hU6LrCfP_t3ax5>~;1efjN2JGs$De;( zPkiztoEc;s^I6H`)X~20!A->iFX#L-`dqkmG7W2@W$aMkv%6Rb9d*BH5j9g3MX2To zN_Nu=lbjtk!mpzO7$(ZWaAc#&31S7Q}C%Gzd~+@R4s<`yda{C6XG^9OjuLF5v#G$|Ia`TZmHZfx#BOyA0AGg507mY`FFe8DOlTR>2&PZfG%a6zfKLB4rD z`HvY@jR=aX?Vs~wbLfE9(T@TmRr{?p9q|XsA=y(~MH%P1-5=a-WVE}-IrpGhyyr2wb zgETVK`c+pKd{Vu3$9|$`Z10h#)4OVpZ6p>iN7qY#hJBi>%|>*R-fQpMzz*i&=!=e)k3Nb# zGn3GBaN04qNsj8#oJ`?nDspuGXZVOvjZ(lI+%yvA_L7`Yq1uAvn+6`^{kwzjd5u

BW;M%ZsETl%?a+!9*+Clv?n+$|BNr(~Y^8k;r^p?oI%VZmm!b=y2sm z^IGipq)h^@s7?!Lu&ANJjcU^xQ;Cr$VofkHdEn)ERIgSl;Mw?gE)ew!b8OIY3UDv> zj(U@$8&rCAVfHwL&)i|sCy&#+j@`#fXa8|#@S_j%Bwc2>1&aBx-2;5<7R;yp)m`Lx zvvm~_#hNxU7*W72wRZzBtG%%2EeolGyf~p6CnFiPdCzr?^7>4DBwY~01oM`~BZD2#|n3}Wx}$u1vuoThjEF?J*1>z#$o-D94g?Sco|I&o8bfnWWGe{8@DxOu7>jv~kz*W#{cH#Uq9K~Gsgk((|YDCJCB=~@+1e9U;@|+09Qamza_aa7= z7;90U7%Qel(bWjJ-=Y_n|4;4jBq{zrOfy&cmG19RN9AT){#ve!v(gINNcV*Vz zdxGpOhr(iXXb_aWZ=IuDEGrY-n)*BGm?FhhQkKL^Si~6_u~oHXW0F_Px-||INXXK^Qt%As#YrqljNjq8)OK3))8*DzYx^Sq; zIBbyAb0*i5q7tYR#^r0>20ll=usj*MlUh#XYiMNl{$LluO;Y>ypdl5(LW~`vwkB_2 zusBevpelu@SEWf0Uo|dY1zrVJRaF<7s4+s(_NY{cEdwUK3phP!qGw!WFe1D^=3e#E z_7|h@G>NUrMyU~85}B|D50rod%&m3Lo1@`}Mx{K+8>Jh?Bm`q1a>^VW0TyVKpgp`4 zDJ@aFfl7i1dKesxK~_>n9~#7rh#!V><=bTsRaMns?(5r`vO8$@5yka4U95!P1S=kh z5nzb}hjp=a^AxJI_}w^JtNIfis62ShG_Au#@sOgbKpIkvZ$gU!XKGro)xA?x?yn1| zZla7xp!sul0nKR~b90r8*NOD|io!WvIrI}dR*(sNL^2w$WsCt~7FASVwZTq=*G&q)QngsMX!<_M}KgO5oJn&(aGtOOD) zAO#}4Nn)m(maJc8L8 z)I++c}F>agHy?oP~v@erLy7-+6?*Bn(=p(?;(`N_=^Y5qi#P#hhbR>lXksCNFHMYrl} z602y7O2T3ol3Hr^=kgBck@f;71($W`ON%lmg74J$Wu_TznnyxP5cQ~NVNRiJmQmQjPJVWlTcJ2xH}~p>rY5J}m*&8+FF)qA zGtJ!4(eX+-pbAvbMyAW*a`eWg7%nvUn;1xjsx6*!w*)~7z9yY&phFnCYIau}0TWWu zMhK@SnyDr|R$G`Uw+HS@Cvn!+LDK zXh8z=}Hupb{(DCrqqO_Sy&F~_SAzkTerKZIecw2Pi8cXe_`X=(6l8Du;DmGCIZ{6Y{3?>IGa`*zpC(-c zS|k)0R1ln)^c9`}43fmAvcUybBvF0{7W^9j(_t%Fw5XIAgFED_4TdE`TEvP}QXM{g z27JGSj3$0QU8u05bJ~eUStQ6H^g~nfqs(mOAK0171YDK8kwrFQ^Vd{61Xu2UweT>u zK_tyNi%K|RflQDbxNiVNKq)Vt^ilk{B~TWr496sGH%h?-3KEysC}ZWuC&2T>DUBK) zR;k@}$oymK1FzwmAm+S z|I!#>h6jJ2q=D2+hXZ0^Y{O9p7WF->8?z9mhW$Gw`l0}iP=>q3q~46M@lw&U#3~&K zyyIgmWehwjJ-lSZ<+qWcV|RzM(W7b_)2Y+i6Ex?KVMcHd1=H~8KSi0#qs*Q@8>L7H zp^j~*UIb@+Bklcp=crF9;Hdpyk^9YQ%M-z-^P1TnxjK%x-`&SwEt-Ziu@Vt(gHAyeHvCE}k!=#zDUTmeVu#<;E6 z6`MkEj6UPecnb%iT1yKNtz2421V9eyub6d7YoXHf6r}juovyp#B27_5Twt2d+kS6T zvgcrj>m}3WFfNI|%xeeQIb#^Pe&3xC>G_b^HqN1XpK&5v`ZM#@#qDh~sXfRTFM~OC zhX^V3M9f&mAS<=t9C|6UEh4So_9cvtaxMTfVduNfU&ECFyoPh0SCnS*-_-Ap8$$KW zTJNadlCTOJhj?;Slxq3^V?$T3{ovKW`j}0w`%8a_`NS>R3N0vpb9>y#yz_OjQ#x9E*^WY_4^*7bE`p zD?jTZvN*A`_L;ucCSTVfkkx)X*Y2%=w{he78_-3K?+k6-U|sx=`SB6WP1M6S*cbUf zv|`NRRPR1^rT3njCZaIa7)$KLkEOegi5@$93BnTYJOXclpgw>f8gvP40bO6F68q)c zR;PsKL$Gz5PRv;JCBW-#5h=?Ig+9IZL*x#p@ClCA$z?iZJpoJtfS?7!!O#L@jkSu~ zA2?eNI|Ym+oPIVR)OKD~YYDF) zSn)~MkgY8>?~dV1D=Y6@OEKreMnTwxcZh8fi`?)XZvR~(vAKCw6_mrSo$RR5vv#>>Dc`qJtBDDo{w zX=C=y-*fZ6)5vyfzNe?B{igfM(f?57!7GtkG7@*`=`~JzX2o9VKYBN4t;I~ZGOj5W zIwg-m$F!|@NglrNJ3EF}{^VD7?`Q7#F>_}vqKPx3OJgV%2soL#_VWS_V|A2}75>lL zH=eBSm@`5*ndyni@NVE1?AQ3q&C^!AuvUk_u|rJtE0MIr&(ILEgVX2WR!iJ{lV1$x zj}P_dHVR7GuuBmFWdd402eXjaZ5OZ~kpbyX)jz#dmQ$R;i90hQ3rWNlM)!yH)az&; z5s(ZPH~!llAm4yTA<#`3 zG!sAa)z}Rul}~T*`9;&4_?^E=60|RMlt&t(RvH%S>{z`fI%V0mrhBFZkM=sc`1*hc zI))>cL>t#Aqh#ivxzIw5C_?!6(_~Zx3PWH+CqI*&Z;4vggIHsgk}^+b0U`@TU9Xum zJaHrGzy}RlXxLxoWXJuzQ_B>+t2>-4v>EH- z9Mj36ON2wo#ZHKHYY!%K`znbm z6*)4|?L4I-o;$7dTRpYz9j;qlEt2bNj72?UN>C-Bkbv{rRun}oB_DTTBjeL*w)7Yz zmYFJPS}hWwpz-K>-?lZIVcLH0-FtiO;OxG;x?4v4lD6ZCi}?7$L>nU1#?P@D=ePM?!qzu@bAr9k#&BK%FN>s_Y>S{;{ddDqVh4;CS`=-$`~ z3cqG*d2Sipxl$r^kfH3Mz`lfyt}7EC^A&{{L? zm;yi$6U{lK00uby?pOFTI5XBs@RhbXyBX>z&&vR=77R|mA8V#9%N|2P{hZPhL}(_@ zGXLD)HX`Pk!0WBoeH3X6H=*$AjLo9}!Gi!g?(1fynFc|bgKa+;@bRzuL4Sddy`sXE z{X5Ui?(?zv-0pUsx83+X9S@a^8p2r1fp{E>DRq+Ag|64*DQ~XbaCPDK43!(Q`CLrT z6FYwr?|XB4wqwQEO~=RAxr+#Zjw%%RB-xL>>`>wj#iSY|O58b4n3B*!98lZYl^D){ zAjeghlNC)x3Vk%;WN zc-whuy890r#tnv@IjFpxd}Hf$n@)V5cc0S|fo7_*-Bsq#BU#8?jkghOLabm08K5CI z07ST$saTGm;?i`;LxD4prlf>p5U7Y$ss!RYb>ybCtA;2X!6C+i^dhfS4te9~#BSr+ z(}CB<(s>$G@h}sV#N~BiN}WJRPER2Et|ih7AP2X6H(6t2X!`m%~pl4EG{9Z|f*Tg$J?diPN7IV6_ z9EeSQi-zH2Zqs&dluW8*>z)FgYDDDOcH=zHjH zP8ccGq~;`#QWXwUTre3C#1a5-1O((O$&`b^Ar(6j4fv}ibGR!I#&9-Z<2gY|k_&Pu z`zUgIA8YG<)#qi|^>#dbx=~GKhb>{0kFhPZqibTu_r!O@@+fX`P!Gwnf#EDyQrAQg z>sgr=g_<40pJm_gSv&7hLsUSdP7Z%J9+;?l3`JiFvz-XNtDUO z^oymREfxo=H2cePU)7e?ZYaz#dlf$kw908TohfhD{QWKsh1uu2v(_XN5-Qw3!~}qw#v-9FB^o4W9OBVKMl7% zS;+k|Jz@C$7`8#FE}5GZUYR-Ek?<5q80dC7e23x2^pn#ivUL$MfU>6Z~MB{h! zg6axOVQqzgMBP=&p&?BL1RO?XNNve80sxrYq98}TnhQUoAd&$J1`1L?MQco)6<&u# zXaiDj5aZgd>mfh|wd(1NP=_|P8}5ZOLLuFFN(QD|&lnS7OYceHmM?Sm3-n)51N zOqlxT>H5Xhi^y*-Yh}fWN1P+ubM9%7lqgYg4BX<#F7pZ`L*rEmb`_;y(Iy0#DK5!k zW{Eg3gCn&yH*xsOzoP@n?_qw1g(XEFd(ET}KE?ofFX=O4PM-Y`GQbFS6A3#n$$czy z*nK8=d|h<7;%X@1949r3BGuT+xw|F2wvzh`mCMzyfzc#Ua;Oy7KQ0WU<*Tt_l)cms z>(L5%SQurYxjizVjE_Sp6snSl06@47vr3g1<`I1C@9(qoMCJz;%G2rOfJ~?M`9A)9 zzm0rH*f$`xMdJ`{I}jis0AT177!a*(OB0W;*m1DUg=6F*et$o{@4cn&gjI7z0|>tI z1t8-QC$ zK(l0YDZKMI|(dB8YL|gN(}#{wMxN33Np~v;^m6q*BpBkz^tG z%;^1kk{_hNJ=#wmzJ2m4^wC@^jn{vxTJT>D0A09>YDElUB!*e<;iCY1*d9`Os08(O zsW^4s>4>plu~sUm#TGFIVgmN>I%hkoks{$CO(dj(i%eNnQjucFD#eggSTR9hq*w~8 z0Xy^*!DLvICy=!O*vN7JCV~H|jfs!S^(^}d!7|R`5^hO2xp<@k)5T>TW8C5KJ@s{D zKYoY(s1s{Clb_P5fnJp~A2SV@sc0kJzzUDY&zy(d!7UIsI&WQPi5~_48DN9ydNd87 zZS}>hS9e9HQ7$tQlopwQ3R0lWScgxlqP$2Az^3yiT6!%hcKQlXGdGYUdM4Jk5l$Ww zu}*Ou8eIN6I&6@UVt}Sc_3v9qe9XdS1+afk2z(eCy*B6DYx#YAOZRJ!H??V3i5@pi zMwIc<^N~+Y6UI&1BE!wrmfdg1z=+`uc+e1vlA|I5%Z{MeW2wRJ( zy=%96B`%o-GJRedb!A^rHe$bD6WLDl4|?(0N0N}j9@z_W?`e?Y`tPH+y!?z!Q*UJH zZ-39AJ&kG@*VAmkhs|%tTX$CE3fH}GP3H;FE<%w(kq3CFC6Q?;+UwGhJJ%*FTA~Et zU`fQpNI|B?bVxWUzs#gm`)xoXKw*}RtGD>i4E4qNq+}$pt_oH2tymqzCeeNOdfXbj zW*=O1^sHf5KUIPUAOK1TB3b*U(<4(js1>^HNkUO{tF9^aDl3^uQs8S4KK93;Ht?XQ z1YD!WAn;6P+c*p`aKu+0yvF>Qlt-9FLnmxoOW*X){|K$BqtY^fE;TSxLJk|NNl-D3 z)}~yM1f7-5k{~8h4Oz^I$lj>;gnS>LV_re{s)4~BqJ=;_m@dYTH-9bv6M;dN3+Jw* ztkEg9wrekK6G9oL5Zsx<5XKeo>b-?gvjOuF!XDx)_H&D=vDn-pvk5$g9SE{~w^kon z$Hm=q^f=VYd&W)=*UwGEgUNuhGbK&CT9z0zCk{144BVcsa>WTyE5*pv*S8PqHk)U5 z2Yot6skoaW6-Y=mSK&&kW8h8Ts0ncb92}V$&N~^f1d6C4o&^*p=ow26OizaXpn*X^ zD8VUGAcEu$SyBR;Oo&&p0T+ACeu(gL*Dvn)ldOV*BF9Oy^-5q|Mhci5${>NMm_sBH zkjp70Bq(IYeda#juls90?_Hi8ZpCjQYgq^i0P*=s1Cc1{=m5W%!unk99E0j8`9=q4 ztUBEp`mT7ehY^x!=mxX8g~8?d>px`=x`@QI@tn}aD|T?MEkvUP*oiv?f}CQBx+pQ9 z<@8FEwM=5CiMz+k`_NK<>4W#2%e}G1}fLa)=YpP?wyYEHJS}8SLr(%f;0e zf+$@g9BWJ#wS?npxRim&IK8s6B_fggL>}vg0TR6`8v#uX-gVE!y>XgI%ebjBlvi7L zil7pO0YFJ0$i(tCUvnM1=I&zpDf_!I`yAhUyVx5k{qv9(5nw1N2+0%_0Z2svHl(CP zfTW5nBTmkaj}ARV4?);OKV3cxJ-gYCf|$ufFa%R1J1!vwMnIGi42VkHq^e0;jLM5q zlmpLIAWYV=@E}xfzKap?qFOHZnWL)eDRDLFrbGX)$ZBt2UV@MCtCfeedDGBCP5sNX z{tFA}sB}G^VxtHBwvtcPO{jY5@a~ih_x;V2zMMEzkqlq}mCW;2{D@j#?Wop=-$PSl zuKQcFd#YWS3^RNUU4DzXk4y4xNg8B^U?+!xpz)2QLILthAH%fMrfdQOmiJkK!WfX785%qy}xu;mTu)yv)?;lc60AO`~hIE z36$-R=ukt_F)kbJRx^;XE6u$HuM-j>ZAx@-;&3(VSkzSCw4u&|(ohXau2)$b3NbBB z4#-Io>gQD*t<9t(IrGBrX}EP@Z}q(kClYQz&)87$4SEr7pUVQZt4D5=Vkx5(1i~QS zu|_ZsXNM(1Nj2P*3kk6U{ylWfV0(Oa96HZKxE>vA-TZzFQ+5WrW8q=>XFe02S{sO;*wHGD#B-d{NZw za*_rIRGOzZ@2Kpvy25`t>~&6_Te?=8N!@J&n`_Z?OxDgqfC}M85W1?WhUC8e8{1A@ zDABHp4cwgf*6n;~K+}fOa6)Ij9;$He#5coQLl&NDw(0;(r0`RB+~C9H^=&Ob`X) z>J64ky7fBJn%hSQ=`se+ike>@Tua$eE9c!M@=>`4wSI0Tncmp#I0^yLUl`!6V4*4L zR7|u#ipT z>}I*EWt}@Zti#rtAqci4ujKH*MV|?oIW@BI-TQ}gX!0~F#K!`I+((8t+-q9XJjoAT z8~&v5Eb!%|Jy91U6tq_?{>ZLr@Vy-dInB%}7P`HjhPB3Nd|xT-X!sC-W~F>{sppj} zFi_|lAy=KL@5vZyV z>w&aQrBgfmKI>)V}=Fo-W=j z5M#?z>#}%E@pk;asn>x>=!$U*CM2BfIXNmOV?lEixRbAyQnxFRg~SRZ4;r69*0Qo- zdu+8|W`ty1Vv^xRyUR+dC{rDBP38rOEEnt*1=PDr#S{^Yz#t1NQ9&3=TvF=aa&UbX z8g1KQ<7DH2hyTAHn~krXy1zQB2~jym)@SWf zV}*1fXx$ZQLi()zZNI(!w{xpbwnM4po8#zQCGaR%zs9S$8$Es41(-1cf|M}K5)xl? zAocVb0wZV&(&B=Ew2FYNq7C!#I_0-(5F6Mm3pM{!vsfnaT%{e3ZPH=4yO6^JuRB=i zSeg^s+9g|fRC~ILS@*pKxpIZZ-=@>i8m-}*k7`0u@D@g@*LthYcP{Da*;B705djc% zq;K;o0l~+#)%-<*kt^(U0)LSVZl$~i zxw9DHYd0W{Zj|+$aW3^b4kvo1-+hs>dkIbc2OlNwDIUjk)sgiDGL#Bvt z)l7D53KRB8gpz&21$Zc?0nExp{Z37Vs)rP6A?|0Yiem-wQ3@~OTG5k2B}IMj=TIAR z6I9QRid5jxiS?PS*Vv$_ni_7iu;mkWWPp~2pM2}_uT&VoKqcVId2a|WMXcjBon@u! zp<7(X+d+wgfxpFNSmfo)m6+p>D+?v$Khag)DeKJfGt1+7u66fOKk+SI{`$3Pg+l$G zbK14GwX)_88wE`22oS_z-)#Db;2b^bRS)`m?(hb~)$S07=+_%BKSDB;8)}1$7zXa8> zn=99%n+?s0sRF21L3XCKW?QqXrl<~6(v(XE1wo@_{Iekdcm2Bje^CmSTP6ob+#DLdi~k30{5lSGZTgXGHb`6WYFqgqPLFM+0;MB>bVKI#8Jf=jY?za>u*~E%cWlvGmc37!d6X`)zF^kIr|erkf9I2yLlJ_+|83CPcEdgr!8P4mvy9ku|cprVu@LhJ23}NpX=#~z{UPZ44 zY%#RY8cd!ZM@%N0rDIQT9o%Ugj~KlLd^tCYSs=)%90{^>Nw;wK5Pur_5b#tZLbEN4 zS!qw&W@uPMAfKK>J7#!eqM-$%IY+Gn^)wr=*_D=zZGsC(-NVEqXH+t8|=Cd<-- z1-niK9>nhq0lz`pE7dZ#h=mC1sUw(|74<(Zt&VHBvYVqP&+X%jHZ-4oWiTF;DHVQa zy8FMA$J%SEhub^V|Hj7xUiSs<)oJ36F|?&YtR&PC^5I}iyp8q)sSRGSn0jL^I{`>WIJSHxlP9x)g5YwqipO*9E^9dOtr zsZ2?VD%wy|Pzezu)|3Irw7e>mB_9)Icqv&)5G{EIM6PN`b4m8(aw<@Vx!^DfqF!n; zo2pRqRF_1*YEW;kTe^)-wZN}3+t{iOlQM+nlmf{rX%wJ|ACsk9O$}5lbxIS^5v+Md zKBd>Ju9sn&4jqc_J|xr6(&2yM)HXf_2eb5QYxFqT-e%=``s=)(iRahiJ(t1g@xIpn zi&MziN0h@a27DAbnLQ*A9pGcJb6w{jr_1oRu{ZT7cOpDObRd_&zPE`}gG#SEZzWVQ zbVEld9k$XR9e_(zfO_3@N#vme4At7eIUF)?cc2OdF=r0_+1+@s=Gj59?<6|YXIEX* z-F_+?QU??!Q08CNz?2d3y6#QY?n7Ss*ALzhh5hev-R?z8XEJ5_3ef-?~u5 zRN+vYNReWtBSl?4_^kYChCqp7MLrj6=rL0Ul{yTik#*Ovuig9?q>h`BE%3_#B5t@k zC5L|h4`8%zM{cp?Q&1CjZN_>SQNMk7-Zgmr$|_1L3-tYZum_OsyqbL_&`b{IJ4&Kt z${Gw>Tr2X>12`yDEEh&toyOs~gyHI_zq8FoSQJ#LX%-S>QXz3iP_@feRGLUK>f00r z^|@rhXWMA&*DEDcy%wrwDQz9wCgb9eAz4nN_6Q8LPcIIaDK2*e4Q)4L;^5ba!ORHG zAEJJn(udS^cCt=)E)^GwdvyL&hIYG^-+Z4Yyr;-Xzr8{2ke*h&?esiT_G}Nq@bB*(wGJGU-{;aOb;7gcvd&0&tk6p=qspUkk>gg-$3&YoK0&2*-U z1+R9Wdf$}H%_^F4V86I*?qj>nT>SNM&=D0ettsfT+t^@v>1-+;)S%C$O|PeyHppCi z_&3Em7a(?wmZ(gjmUBGM;Z=U={HiL=EA-fYHw>0er_!pnZ)fFQFU?Mef0g=HVdhSZ z*Ye%Hd@QjO^_u=asdjurNxX#iMFSs$>6d~SC_c}}a2$`fUzzcOf#&Ly3D19VYc}qqp}cPFtl6B{ z$~cL2SKw+nk}U+t7I|3+eQ3N+CYescJBY@`Q>RgWFtiEyWHt=@0EZMM}lnpD;k3Nbj`S64sh~n zGsqZ7sDeiD_oW4z(uYfEuRXv5W@JH%vctiG974k_1v#~;21HLltM>Vklgwj(L(sjs zgP^fDNE)9ic_=C>!r2h4_OSjA{M&Hler@dNHHgm_T-n5Kh|xIn)?{F=&OgI=_oV>J zKo=09s|~5t&BC)|Go?Voah#q-O*NtlD1nD3trP3Fe3*h4n3)A2C=04g2!a7)8pH@)bxFdHfs4hv7&V(-h=8@F$H-VY}q63)K-veD!JFuL3b;brI_`aMhpVb zV;NJ7g9kz^m)A*zUE&c>{Ly2r&|u7f2w@MF(z=>aFPb8;j`zu2V}t}=)kHOohtfi& z#zaBR+EC}A_GikJ2pjaj+u}9@U^f)QZmSNM&!7E;Vc6(~%4!pb>v9Xf9&@9}&c@pA zZ_vNuAxwkQteqnE(F}wmh=@=P3}3)VAn;_Jg>ikK?jIMh>h!ivvQbDEe6yp>qH`nD zC@%$HMS9Gh>*IYdd4PtgK92}f-mW=sB&m2hnV+62gg9#Acu*@boW03Sa(ksfIK=2E zv;7v1f2(1uhHpaNl=8m!HhxP&A9mY@;WU(P97W=cWgQ( zywYcLi3N}_Fu>#&OZ+%K%RzkT)z0$;U0tr4#Xv^9_N`i@S}4m_=Tm@!hA(Clzd~<`nHt~Utt0~e@?^}VsRT!ryfqn$(i7p65K18U_Z&0 z9wI5X1U%@4_f`YK4DleyVBG!{fjL5>ZEX1doP`h04lAY*jVZeMFbDFOK67q12nLMn z?{#|4b0L%6pvF+wEuaY=5=AOwg&ow*)(fW$o~6K%Y6Jf~SMl=TK@nxgp^ z(l2Z0wKO+ruT?>!6!T+iX)|Z3yl4u^yA@K%P86InJv~?M7QZmZSmHMuz)%y;PuJq_ z8++)q{gjULN2#+$7bGi%7OOupiW7RYM&SeY(Lc#z*ABF*cMauC+JXl(3a_?8w} z#^imp*^}J;ud1Hj!n3jxPobfgp|XuY*DMahH<9nqV${r2H0XVfmtVc&$!l4< zO??#Oc^@(L_R#?mV0tJ1bIWOFAprqh6Zp0OEyos+rK(xzw2Ym7#1zDf9WfKT<>s&x z+-r(goV~-CYu{xvFH9t>#T7kkq@kE&O-WHQ)C1dPtxM7Av)NeMYg*dtnPdYp3q@WC zTMptMY5Gr3@qSIiUs{fCo}L=NBS81b#q4}4lj|q)5>M~;*AHuUBJ_Vra^L)!()dvF z7#g#V%{*V{1&zI)7tJgAUqjY2`hORw=Rkn73Y{u=l`u~l6^9f7jhjZL_cx&Q%ITkf z>Bu1XU4)4SEDmUSfdEKB_PCq(!0{NYdfEX#;JFh4WI^-mZ9+0S@;XcPOan#@Uq=`S z(kMId5`W^z5K@>ZyA_#clPUZM85uRT7e5MNRM!Yn+A+LcIOSnoXXBC)3HUDqK@Rg+ z9hdbstE-j4MY_Kw5Eg|te;2XZKqB%62VX(yv!2BsO~c7UCRsCO2g*ce!

^aKX)H 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 O;_gVN3K9a`DoQ|XZiR;c literal 45892 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 UboM_(lt27k$rRy2K>Qy9z{TtP>;M1& diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py index cd57e317bdeb..ffb89a4dfcdf 100644 --- a/worlds/pokemon_rb/rom_addresses.py +++ b/worlds/pokemon_rb/rom_addresses.py @@ -168,12 +168,12 @@ "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a61c, "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a62a, "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a638, - "Event_SKC6F": 0x1a666, - "Warps_SilphCo6F": 0x1a741, - "Missable_Silph_Co_6F_Item_1": 0x1a791, - "Missable_Silph_Co_6F_Item_2": 0x1a798, - "Path_Pallet_Oak": 0x1a91e, - "Path_Pallet_Player": 0x1a92b, + "Event_SKC6F": 0x1a659, + "Warps_SilphCo6F": 0x1a737, + "Missable_Silph_Co_6F_Item_1": 0x1a787, + "Missable_Silph_Co_6F_Item_2": 0x1a78e, + "Path_Pallet_Oak": 0x1a914, + "Path_Pallet_Player": 0x1a921, "Warps_CinnabarIsland": 0x1c026, "Warps_Route1": 0x1c0e9, "Option_Extra_Key_Items_B": 0x1ca46, From 19b8624818212739199a50bb73275649d99c9d9d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 10 Dec 2023 19:11:57 +0100 Subject: [PATCH 022/282] Factorio: remove staging folder for mod assembly (#2519) --- worlds/factorio/Mod.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index c897e72dcd11..21a8c684f939 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -5,7 +5,7 @@ import shutil import threading import zipfile -from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple +from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple, Union import jinja2 @@ -63,7 +63,7 @@ class FactorioModFile(worlds.Files.APContainer): game = "Factorio" compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives - writing_tasks: List[Callable[[], Tuple[str, str]]] + writing_tasks: List[Callable[[], Tuple[str, Union[str, bytes]]]] def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) @@ -164,9 +164,7 @@ def flop_random(low, high, base=None): template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value}) template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value}) - mod_dir = os.path.join(output_directory, versioned_mod_name) - - zf_path = os.path.join(mod_dir + ".zip") + zf_path = os.path.join(output_directory, versioned_mod_name + ".zip") mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player]) if world.zip_path: @@ -177,7 +175,13 @@ def flop_random(low, high, base=None): mod.writing_tasks.append(lambda arcpath=versioned_mod_name+"/"+path_part, content=zf.read(file): (arcpath, content)) else: - shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True) + basepath = os.path.join(os.path.dirname(__file__), "data", "mod") + for dirpath, dirnames, filenames in os.walk(basepath): + base_arc_path = versioned_mod_name+"/"+os.path.relpath(dirpath, basepath) + for filename in filenames: + mod.writing_tasks.append(lambda arcpath=base_arc_path+"/"+filename, + file_path=os.path.join(dirpath, filename): + (arcpath, open(file_path, "rb").read())) mod.writing_tasks.append(lambda: (versioned_mod_name + "/data.lua", data_template.render(**template_data))) @@ -197,5 +201,3 @@ def flop_random(low, high, base=None): # write the mod file mod.write() - # clean up - shutil.rmtree(mod_dir) From 01d0c052595ecd61b61d51b2b3b766b3e4cd22a1 Mon Sep 17 00:00:00 2001 From: Rjosephson Date: Sun, 10 Dec 2023 11:12:46 -0700 Subject: [PATCH 023/282] RoR2: Remove begin with loop (#2518) --- worlds/ror2/__init__.py | 10 ++++------ worlds/ror2/options.py | 9 --------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 8735ce81fd5d..6574a176dc2d 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -103,12 +103,10 @@ def create_items(self) -> None: if self.options.dlc_sotv: environment_offset_table = shift_by_offset(environment_sotv_table, environment_offset) environments_pool = {**environments_pool, **environment_offset_table} - environments_to_precollect = 5 if self.options.begin_with_loop else 1 - # percollect environments for each stage (or just stage 1) - for i in range(environments_to_precollect): - unlock = self.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1) - self.multiworld.push_precollected(self.create_item(unlock[0])) - environments_pool.pop(unlock[0]) + # percollect starting environment for stage 1 + unlock = self.random.choices(list(environment_available_orderedstages_table[0].keys()), k=1) + self.multiworld.push_precollected(self.create_item(unlock[0])) + environments_pool.pop(unlock[0]) # Generate item pool itempool: List[str] = ["Beads of Fealty", "Radar Scanner"] diff --git a/worlds/ror2/options.py b/worlds/ror2/options.py index 7daf8a844666..abb8e91da25e 100644 --- a/worlds/ror2/options.py +++ b/worlds/ror2/options.py @@ -142,14 +142,6 @@ class FinalStageDeath(Toggle): display_name = "Final Stage Death is Win" -class BeginWithLoop(Toggle): - """ - Enable to precollect a full loop of environments. - Only has an effect with Explore Mode. - """ - display_name = "Begin With Loop" - - class DLC_SOTV(Toggle): """ Enable if you are using SOTV DLC. @@ -385,7 +377,6 @@ class ROR2Options(PerGameCommonOptions): total_revivals: TotalRevivals start_with_revive: StartWithRevive final_stage_death: FinalStageDeath - begin_with_loop: BeginWithLoop dlc_sotv: DLC_SOTV death_link: DeathLink item_pickup_step: ItemPickupStep From d3b09bde1274fae368ebb2965d6c40a1524970ab Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sun, 10 Dec 2023 13:15:42 -0500 Subject: [PATCH 024/282] Lingo: Fix entrance checking being broken on default settings (#2506) The most serious issue this PR addresses is that entrances that use doors without items (a small subset of doors when door shuffle is on, but *every* door when door shuffle is off, which is the default) underestimate the requirements needed to use that entrance. The logic would calculate the panels needed to open the door, but would neglect to keep track of the rooms those panels were in, meaning that doors would be considered openable if you had the colors needed to solve a panel that's in a room you have no access to. Another issue is that, previously, logic would always consider the "ANOTHER TRY" panel accessible for the purposes of the LEVEL 2 panel hunt. This could result in seeds where the player is expected to have exactly the correct number of solves to reach LEVEL 2, but in reality is short by one because ANOTHER TRY itself is not revealed until the panel hunt is complete. This change marks ANOTHER TRY as non-counting, because even though it is technically a counting panel in-game, it can never contribute to the LEVEL 2 panel hunt. This issue could also apply to THE MASTER, since it is the only other counting panel with special access rules, although it is much less likely. This change adds special handling for counting THE MASTER. These issues were possible to manifest whenever the LEVEL 2 panel hunt was enabled, which it is by default. Smaller logic issues also fixed in this PR: * The Orange Tower Basement MASTERY panel was marked as requiring the mastery doors to be opened, when it was actually possible to get it without them by using a painting to get into the room. * The Pilgrim Room painting item was incorrectly being marked as a filler item, despite it being progression. * There has been another update to the game that adds connections between areas that were previously not connected. These changes were additive, which is why they are not critical. * The panel stacks in the rhyme room now require both colours on each panel. --- worlds/lingo/__init__.py | 6 +- worlds/lingo/data/LL1.yaml | 112 ++++++++++++++++++++------ worlds/lingo/data/ids.yaml | 5 +- worlds/lingo/player_logic.py | 31 +++++-- worlds/lingo/regions.py | 15 +--- worlds/lingo/rules.py | 23 ++---- worlds/lingo/utils/validate_config.rb | 2 +- 7 files changed, 131 insertions(+), 63 deletions(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index a8dac8622162..f22d344c8f51 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -104,9 +104,11 @@ def create_item(self, name: str) -> Item: classification = item.classification if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0\ and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.painting_mapping - for painting_id in item.painting_ids): + for painting_id in item.painting_ids)\ + and "pilgrim_painting2" not in item.painting_ids: # If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings - # go nowhere, then this item should not be progression. + # go nowhere, then this item should not be progression. The Pilgrim Room painting is special and needs to be + # excluded from this. classification = ItemClassification.filler return LingoItem(name, classification, item.code, self.player) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 8a4f831f94cf..ea5886fea00e 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -373,6 +373,7 @@ ANOTHER TRY: id: Entry Room/Panel_advance tag: topwhite + non_counting: True # This is a counting panel in-game, but it can never count towards the LEVEL 2 panel hunt. LEVEL 2: # We will set up special rules for this in code. id: EndPanel/Panel_level_2 @@ -1033,6 +1034,8 @@ Hallway Room (3): True Hallway Room (4): True Hedge Maze: True # through the door to the sectioned-off part of the hedge maze + Cellar: + door: Lookout Entrance panels: MASSACRED: id: Palindrome Room/Panel_massacred_sacred @@ -1168,11 +1171,21 @@ - KEEP - BAILEY - TOWER + Lookout Entrance: + id: Cross Room Doors/Door_missing + location_name: Outside The Agreeable - Lookout Panels + panels: + - NORTH + - WINTER + - DIAMONDS + - FIRE paintings: - id: panda_painting orientation: south - id: eyes_yellow_painting orientation: east + - id: pencil_painting7 + orientation: north progression: Progressive Hallway Room: - Hallway Door @@ -2043,7 +2056,7 @@ door: Sixth Floor Cellar: room: Room Room - door: Shortcut to Fifth Floor + door: Cellar Exit Welcome Back Area: door: Welcome Back Art Gallery: @@ -2302,9 +2315,6 @@ id: Master Room/Panel_mastery_mastery3 tag: midwhite hunt: True - required_door: - room: Orange Tower Seventh Floor - door: Mastery THE LIBRARY: id: EndPanel/Panel_library check: True @@ -2675,6 +2685,10 @@ Outside The Undeterred: True Outside The Agreeable: True Outside The Wanderer: True + The Observant: True + Art Gallery: True + The Scientific: True + Cellar: True Orange Tower Fifth Floor: room: Orange Tower Fifth Floor door: Welcome Back @@ -2991,8 +3005,7 @@ PATS: id: Rhyme Room/Panel_wrath_path colors: purple - tag: midpurp and rhyme - copy_to_sign: sign15 + tag: forbid KNIGHT: id: Rhyme Room/Panel_knight_write colors: purple @@ -3158,6 +3171,8 @@ door: Painting Shortcut painting: True Room Room: True # trapdoor + Outside The Agreeable: + painting: True panels: UNOPEN: id: Truncate Room/Panel_unopened_open @@ -6299,17 +6314,22 @@ SKELETON: id: Double Room/Panel_bones_syn tag: syn rhyme + colors: purple subtag: bot link: rhyme BONES REPENTANCE: id: Double Room/Panel_sentence_rhyme - colors: purple + colors: + - purple + - blue tag: whole rhyme subtag: top link: rhyme SENTENCE WORD: id: Double Room/Panel_sentence_whole - colors: blue + colors: + - purple + - blue tag: whole rhyme subtag: bot link: rhyme SENTENCE @@ -6321,6 +6341,7 @@ link: rhyme DREAM FANTASY: id: Double Room/Panel_dream_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme DREAM @@ -6332,6 +6353,7 @@ link: rhyme MYSTERY SECRET: id: Double Room/Panel_mystery_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme MYSTERY @@ -6386,25 +6408,33 @@ door: Nines FERN: id: Double Room/Panel_return_rhyme - colors: purple + colors: + - purple + - black tag: ant rhyme subtag: top link: rhyme RETURN STAY: id: Double Room/Panel_return_ant - colors: black + colors: + - purple + - black tag: ant rhyme subtag: bot link: rhyme RETURN FRIEND: id: Double Room/Panel_descend_rhyme - colors: purple + colors: + - purple + - black tag: ant rhyme subtag: top link: rhyme DESCEND RISE: id: Double Room/Panel_descend_ant - colors: black + colors: + - purple + - black tag: ant rhyme subtag: bot link: rhyme DESCEND @@ -6416,6 +6446,7 @@ link: rhyme JUMP BOUNCE: id: Double Room/Panel_jump_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme JUMP @@ -6427,6 +6458,7 @@ link: rhyme FALL PLUNGE: id: Double Room/Panel_fall_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme FALL @@ -6456,13 +6488,17 @@ panels: BIRD: id: Double Room/Panel_word_rhyme - colors: purple + colors: + - purple + - blue tag: whole rhyme subtag: top link: rhyme WORD LETTER: id: Double Room/Panel_word_whole - colors: blue + colors: + - purple + - blue tag: whole rhyme subtag: bot link: rhyme WORD @@ -6474,6 +6510,7 @@ link: rhyme HIDDEN CONCEALED: id: Double Room/Panel_hidden_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme HIDDEN @@ -6485,6 +6522,7 @@ link: rhyme SILENT MUTE: id: Double Room/Panel_silent_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme SILENT @@ -6531,6 +6569,7 @@ link: rhyme BLOCKED OBSTRUCTED: id: Double Room/Panel_blocked_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme BLOCKED @@ -6542,6 +6581,7 @@ link: rhyme RISE SWELL: id: Double Room/Panel_rise_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme RISE @@ -6553,6 +6593,7 @@ link: rhyme ASCEND CLIMB: id: Double Room/Panel_ascend_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme ASCEND @@ -6564,6 +6605,7 @@ link: rhyme DOUBLE DUPLICATE: id: Double Room/Panel_double_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme DOUBLE @@ -6642,6 +6684,7 @@ link: rhyme CHILD KID: id: Double Room/Panel_child_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme CHILD @@ -6653,6 +6696,7 @@ link: rhyme CRYSTAL QUARTZ: id: Double Room/Panel_crystal_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme CRYSTAL @@ -6664,6 +6708,7 @@ link: rhyme CREATIVE INNOVATIVE (Bottom): id: Double Room/Panel_creative_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme CREATIVE @@ -6882,7 +6927,7 @@ event: True panels: - WALL (1) - Shortcut to Fifth Floor: + Cellar Exit: id: - Tower Room Area Doors/Door_panel_basement - Tower Room Area Doors/Door_panel_basement2 @@ -6895,7 +6940,10 @@ door: Excavation Orange Tower Fifth Floor: room: Room Room - door: Shortcut to Fifth Floor + door: Cellar Exit + Outside The Agreeable: + room: Outside The Agreeable + door: Lookout Entrance Outside The Wise: entrances: Orange Tower Sixth Floor: @@ -7319,49 +7367,65 @@ link: change GRAVITY PART: id: Chemistry Room/Panel_physics_2 - colors: blue + colors: + - blue + - red tag: blue mid red bot subtag: mid link: xur PARTICLE MATTER: id: Chemistry Room/Panel_physics_1 - colors: red + colors: + - blue + - red tag: blue mid red bot subtag: bot link: xur PARTICLE ELECTRIC: id: Chemistry Room/Panel_physics_6 - colors: purple + colors: + - purple + - red tag: purple mid red bot subtag: mid link: xpr ELECTRON ATOM (1): id: Chemistry Room/Panel_physics_3 - colors: red + colors: + - purple + - red tag: purple mid red bot subtag: bot link: xpr ELECTRON NEUTRAL: id: Chemistry Room/Panel_physics_7 - colors: purple + colors: + - purple + - red tag: purple mid red bot subtag: mid link: xpr NEUTRON ATOM (2): id: Chemistry Room/Panel_physics_4 - colors: red + colors: + - purple + - red tag: purple mid red bot subtag: bot link: xpr NEUTRON PROPEL: id: Chemistry Room/Panel_physics_8 - colors: purple + colors: + - purple + - red tag: purple mid red bot subtag: mid link: xpr PROTON ATOM (3): id: Chemistry Room/Panel_physics_5 - colors: red + colors: + - purple + - red tag: purple mid red bot subtag: bot link: xpr PROTON diff --git a/worlds/lingo/data/ids.yaml b/worlds/lingo/data/ids.yaml index 1a1ceca24adc..3239f21854c4 100644 --- a/worlds/lingo/data/ids.yaml +++ b/worlds/lingo/data/ids.yaml @@ -1064,6 +1064,9 @@ doors: Hallway Door: item: 444459 location: 445214 + Lookout Entrance: + item: 444579 + location: 445271 Dread Hallway: Tenacious Entrance: item: 444462 @@ -1402,7 +1405,7 @@ doors: item: 444570 location: 445266 Room Room: - Shortcut to Fifth Floor: + Cellar Exit: item: 444571 location: 445076 Outside The Wise: diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index a0b33d1dbe58..b046f1cfe3e2 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -190,6 +190,25 @@ def __init__(self, world: "LingoWorld"): if item.should_include(world): self.real_items.append(name) + # Calculate the requirements for the fake pilgrimage. + fake_pilgrimage = [ + ["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"], + ["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"], + ["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"], + ["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"], + ["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"], + ["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"], + ["Outside The Agreeable", "Tenacious Entrance"] + ] + pilgrimage_reqs = AccessRequirements() + for door in fake_pilgrimage: + door_object = DOORS_BY_ROOM[door[0]][door[1]] + if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none: + pilgrimage_reqs.merge(self.calculate_door_requirements(door[0], door[1], world)) + else: + pilgrimage_reqs.doors.add(RoomAndDoor(door[0], door[1])) + self.door_reqs.setdefault("Pilgrim Antechamber", {})["Pilgrimage"] = pilgrimage_reqs + # Create the paintings mapping, if painting shuffle is on. if painting_shuffle: # Shuffle paintings until we get something workable. @@ -369,11 +388,9 @@ def calculate_door_requirements(self, room: str, door: str, world: "LingoWorld") door_object = DOORS_BY_ROOM[room][door] for req_panel in door_object.panels: - if req_panel.room is not None and req_panel.room != room: - access_reqs.rooms.add(req_panel.room) - - sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room, - req_panel.panel, world) + panel_room = room if req_panel.room is None else req_panel.room + access_reqs.rooms.add(panel_room) + sub_access_reqs = self.calculate_panel_requirements(panel_room, req_panel.panel, world) access_reqs.merge(sub_access_reqs) self.door_reqs[room][door] = access_reqs @@ -397,8 +414,8 @@ def create_panel_hunt_events(self, world: "LingoWorld"): unhindered_panels_by_color: dict[Optional[str], int] = {} for panel_name, panel_data in room_data.items(): - # We won't count non-counting panels. - if panel_data.non_counting: + # We won't count non-counting panels. THE MASTER has special access rules and is handled separately. + if panel_data.non_counting or panel_name == "THE MASTER": continue # We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index c24144a1609e..bdc42f42f5ec 100644 --- a/worlds/lingo/regions.py +++ b/worlds/lingo/regions.py @@ -4,7 +4,7 @@ from .items import LingoItem from .locations import LingoLocation from .player_logic import LingoPlayerLogic -from .rules import lingo_can_use_entrance, lingo_can_use_pilgrimage, make_location_lambda +from .rules import lingo_can_use_entrance, make_location_lambda from .static_logic import ALL_ROOMS, PAINTINGS, Room, RoomAndDoor if TYPE_CHECKING: @@ -25,15 +25,6 @@ def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogi return new_region -def handle_pilgrim_room(regions: Dict[str, Region], world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: - target_region = regions["Pilgrim Antechamber"] - source_region = regions["Outside The Agreeable"] - source_region.connect( - target_region, - "Pilgrimage", - lambda state: lingo_can_use_pilgrimage(state, world, player_logic)) - - def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str, door: Optional[RoomAndDoor], world: "LingoWorld", player_logic: LingoPlayerLogic): connection = Entrance(world.player, description, source_region) @@ -91,7 +82,9 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world, player_logic) - handle_pilgrim_room(regions, world, player_logic) + # Add the fake pilgrimage. + connect_entrance(regions, regions["Outside The Agreeable"], regions["Pilgrim Antechamber"], "Pilgrimage", + RoomAndDoor("Pilgrim Antechamber", "Pilgrimage"), world, player_logic) if early_color_hallways: regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways") diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index ee9dcc41929f..481fab18b5a1 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -17,23 +17,6 @@ def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, return _lingo_can_open_door(state, effective_room, door.door, world, player_logic) -def lingo_can_use_pilgrimage(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): - fake_pilgrimage = [ - ["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"], - ["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"], - ["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"], - ["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"], - ["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"], - ["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"], - ["Outside The Agreeable", "Tenacious Entrance"] - ] - for entrance in fake_pilgrimage: - if not _lingo_can_open_door(state, entrance[0], entrance[1], world, player_logic): - return False - - return True - - def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic): return _lingo_can_satisfy_requirements(state, location.access, world, player_logic) @@ -56,6 +39,12 @@ def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld", counted_panels += panel_count if counted_panels >= world.options.level_2_requirement.value - 1: return True + # THE MASTER has to be handled separately, because it has special access rules. + if state.can_reach("Orange Tower Seventh Floor", "Region", world.player)\ + and lingo_can_use_mastery_location(state, world, player_logic): + counted_panels += 1 + if counted_panels >= world.options.level_2_requirement.value - 1: + return True return False diff --git a/worlds/lingo/utils/validate_config.rb b/worlds/lingo/utils/validate_config.rb index bed5188e3163..3ac49dc220ce 100644 --- a/worlds/lingo/utils/validate_config.rb +++ b/worlds/lingo/utils/validate_config.rb @@ -40,7 +40,7 @@ door_groups = {} directives = Set["entrances", "panels", "doors", "paintings", "progression"] -panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting"] +panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt"] door_directives = Set["id", "painting_id", "panels", "item_name", "location_name", "skip_location", "skip_item", "group", "include_reduce", "junk_item", "event"] painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"] From 3a096773331a6a701c1f57402d9a5c778887760e Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Sun, 10 Dec 2023 20:31:43 +0100 Subject: [PATCH 025/282] sm64ex: Fix generations (#2583) --- worlds/sm64ex/Regions.py | 86 +++++++++++++++++++++++++++------------- worlds/sm64ex/Rules.py | 37 ++++++++--------- 2 files changed, 77 insertions(+), 46 deletions(-) diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index d0e767e7ecde..d426804c30f3 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -1,4 +1,7 @@ import typing + +from enum import Enum + from BaseClasses import MultiWorld, Region, Entrance, Location from .Locations import SM64Location, location_table, locBoB_table, locWhomp_table, locJRB_table, locCCM_table, \ locBBH_table, \ @@ -7,36 +10,63 @@ locPSS_table, locSA_table, locBitDW_table, locTotWC_table, locCotMC_table, \ locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table -# sm64paintings is dict of entrances, format LEVEL | AREA -sm64_level_to_paintings = { - 91: "Bob-omb Battlefield", - 241: "Whomp's Fortress", - 121: "Jolly Roger Bay", - 51: "Cool, Cool Mountain", - 41: "Big Boo's Haunt", - 71: "Hazy Maze Cave", - 221: "Lethal Lava Land", - 81: "Shifting Sand Land", - 231: "Dire, Dire Docks", - 101: "Snowman's Land", - 111: "Wet-Dry World", - 361: "Tall, Tall Mountain", - 132: "Tiny-Huge Island (Tiny)", - 131: "Tiny-Huge Island (Huge)", - 141: "Tick Tock Clock", - 151: "Rainbow Ride" +class SM64Levels(int, Enum): + BOB_OMB_BATTLEFIELD = 91 + WHOMPS_FORTRESS = 241 + JOLLY_ROGER_BAY = 121 + COOL_COOL_MOUNTAIN = 51 + BIG_BOOS_HAUNT = 41 + HAZY_MAZE_CAVE = 71 + LETHAL_LAVA_LAND = 221 + SHIFTING_SAND_LAND = 81 + DIRE_DIRE_DOCKS = 231 + SNOWMANS_LAND = 101 + WET_DRY_WORLD = 111 + TALL_TALL_MOUNTAIN = 361 + TINY_HUGE_ISLAND_TINY = 132 + TINY_HUGE_ISLAND_HUGE = 131 + TICK_TOCK_CLOCK = 141 + RAINBOW_RIDE = 151 + THE_PRINCESS_SECRET_SLIDE = 271 + THE_SECRET_AQUARIUM = 201 + BOWSER_IN_THE_DARK_WORLD = 171 + TOWER_OF_THE_WING_CAP = 291 + CAVERN_OF_THE_METAL_CAP = 281 + VANISH_CAP_UNDER_THE_MOAT = 181 + BOWSER_IN_THE_FIRE_SEA = 191 + WING_MARIO_OVER_THE_RAINBOW = 311 + +# sm64paintings is a dict of entrances, format LEVEL | AREA +sm64_level_to_paintings: typing.Dict[SM64Levels, str] = { + SM64Levels.BOB_OMB_BATTLEFIELD: "Bob-omb Battlefield", + SM64Levels.WHOMPS_FORTRESS: "Whomp's Fortress", + SM64Levels.JOLLY_ROGER_BAY: "Jolly Roger Bay", + SM64Levels.COOL_COOL_MOUNTAIN: "Cool, Cool Mountain", + SM64Levels.BIG_BOOS_HAUNT: "Big Boo's Haunt", + SM64Levels.HAZY_MAZE_CAVE: "Hazy Maze Cave", + SM64Levels.LETHAL_LAVA_LAND: "Lethal Lava Land", + SM64Levels.SHIFTING_SAND_LAND: "Shifting Sand Land", + SM64Levels.DIRE_DIRE_DOCKS: "Dire, Dire Docks", + SM64Levels.SNOWMANS_LAND: "Snowman's Land", + SM64Levels.WET_DRY_WORLD: "Wet-Dry World", + SM64Levels.TALL_TALL_MOUNTAIN: "Tall, Tall Mountain", + SM64Levels.TINY_HUGE_ISLAND_TINY: "Tiny-Huge Island (Tiny)", + SM64Levels.TINY_HUGE_ISLAND_HUGE: "Tiny-Huge Island (Huge)", + SM64Levels.TICK_TOCK_CLOCK: "Tick Tock Clock", + SM64Levels.RAINBOW_RIDE: "Rainbow Ride" } sm64_paintings_to_level = { painting: level for (level,painting) in sm64_level_to_paintings.items() } -# sm64secrets is list of secret areas, same format -sm64_level_to_secrets = { - 271: "The Princess's Secret Slide", - 201: "The Secret Aquarium", - 171: "Bowser in the Dark World", - 291: "Tower of the Wing Cap", - 281: "Cavern of the Metal Cap", - 181: "Vanish Cap under the Moat", - 191: "Bowser in the Fire Sea", - 311: "Wing Mario over the Rainbow" + +# sm64secrets is a dict of secret areas, same format as sm64paintings +sm64_level_to_secrets: typing.Dict[SM64Levels, str] = { + SM64Levels.THE_PRINCESS_SECRET_SLIDE: "The Princess's Secret Slide", + SM64Levels.THE_SECRET_AQUARIUM: "The Secret Aquarium", + SM64Levels.BOWSER_IN_THE_DARK_WORLD: "Bowser in the Dark World", + SM64Levels.TOWER_OF_THE_WING_CAP: "Tower of the Wing Cap", + SM64Levels.CAVERN_OF_THE_METAL_CAP: "Cavern of the Metal Cap", + SM64Levels.VANISH_CAP_UNDER_THE_MOAT: "Vanish Cap under the Moat", + SM64Levels.BOWSER_IN_THE_FIRE_SEA: "Bowser in the Fire Sea", + SM64Levels.WING_MARIO_OVER_THE_RAINBOW: "Wing Mario over the Rainbow" } sm64_secrets_to_level = { secret: level for (level,secret) in sm64_level_to_secrets.items() } diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index d21ac30004e0..5f85bcdafd68 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -1,5 +1,5 @@ from ..generic.Rules import add_rule -from .Regions import connect_regions, sm64_level_to_paintings, sm64_paintings_to_level, sm64_level_to_secrets, sm64_entrances_to_level, sm64_level_to_entrances +from .Regions import connect_regions, SM64Levels, sm64_level_to_paintings, sm64_paintings_to_level, sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances def shuffle_dict_keys(world, obj: dict) -> dict: keys = list(obj.keys()) @@ -7,12 +7,16 @@ def shuffle_dict_keys(world, obj: dict) -> dict: world.random.shuffle(keys) return dict(zip(keys,values)) -def fix_reg(entrance_ids, entrance, destination, swapdict, world): - if entrance_ids[entrance] == destination: # Unlucky :C - rand = world.random.choice(swapdict.keys()) - entrance_ids[entrance], entrance_ids[swapdict[rand]] = rand, entrance_ids[entrance] - swapdict[rand] = entrance_ids[entrance] - swapdict.pop(entrance) +def fix_reg(entrance_map: dict, entrance: SM64Levels, invalid_regions: set, + swapdict: dict, world): + if entrance_map[entrance] in invalid_regions: # Unlucky :C + replacement_regions = [(rand_region, rand_entrance) for rand_region, rand_entrance in swapdict.items() + if rand_region not in invalid_regions] + rand_region, rand_entrance = world.random.choice(replacement_regions) + old_dest = entrance_map[entrance] + entrance_map[entrance], entrance_map[rand_entrance] = rand_region, old_dest + swapdict[rand_region] = entrance + swapdict.pop(entrance_map[entrance]) # Entrance now fixed to rand_region def set_rules(world, player: int, area_connections: dict): randomized_level_to_paintings = sm64_level_to_paintings.copy() @@ -24,19 +28,16 @@ def set_rules(world, player: int, area_connections: dict): randomized_entrances = { **randomized_level_to_paintings, **randomized_level_to_secrets } if world.AreaRandomizer[player].value == 3: # Randomize Courses and Secrets in one pool randomized_entrances = shuffle_dict_keys(world,randomized_entrances) + swapdict = { entrance: level for (level,entrance) in randomized_entrances.items() } # Guarantee first entrance is a course - swapdict = { entrance: level for (level,entrance) in randomized_entrances } - if randomized_entrances[91] not in sm64_paintings_to_level.keys(): # Unlucky :C (91 -> BoB Entrance) - rand = world.random.choice(sm64_paintings_to_level.values()) - randomized_entrances[91], randomized_entrances[swapdict[rand]] = rand, randomized_entrances[91] - swapdict[rand] = randomized_entrances[91] - swapdict.pop("Bob-omb Battlefield") - # Guarantee COTMC is not mapped to HMC, cuz thats impossible - fix_reg(randomized_entrances, "Cavern of the Metal Cap", "Hazy Maze Cave", swapdict, world) + fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, world) # Guarantee BITFS is not mapped to DDD - fix_reg(randomized_entrances, "Bowser in the Fire Sea", "Dire, Dire Docks", swapdict, world) - if randomized_entrances[191] == "Hazy Maze Cave": # If BITFS is mapped to HMC... - fix_reg(randomized_entrances, "Cavern of the Metal Cap", "Dire, Dire Docks", swapdict, world) # ... then dont allow COTMC to be mapped to DDD + fix_reg(randomized_entrances, SM64Levels.BOWSER_IN_THE_FIRE_SEA, {"Dire, Dire Docks"}, swapdict, world) + # Guarantee COTMC is not mapped to HMC, cuz thats impossible. If BitFS -> HMC, also no COTMC -> DDD. + if randomized_entrances[SM64Levels.BOWSER_IN_THE_FIRE_SEA] == "Hazy Maze Cave": + fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave", "Dire, Dire Docks"}, swapdict, world) + else: + fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave"}, swapdict, world) # Destination Format: LVL | AREA with LVL = LEVEL_x, AREA = Area as used in sm64 code area_connections.update({entrance_lvl: sm64_entrances_to_level[destination] for (entrance_lvl,destination) in randomized_entrances.items()}) From e2109dba507d97d45fe4d23e3416b894a709ea16 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sun, 10 Dec 2023 20:35:46 +0100 Subject: [PATCH 026/282] The Witness: Fix Logic Error for Keep Pressure Plates 2 EP in puzzle_randomization: none (#2515) --- worlds/witness/WitnessLogicVanilla.txt | 2 +- worlds/witness/items.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/worlds/witness/WitnessLogicVanilla.txt b/worlds/witness/WitnessLogicVanilla.txt index 8591a30d1fbb..779ead6bde4b 100644 --- a/worlds/witness/WitnessLogicVanilla.txt +++ b/worlds/witness/WitnessLogicVanilla.txt @@ -430,7 +430,7 @@ Door - 0x04F8F (Tower Shortcut) - 0x0361B 158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Dots & Shapers & Black/White Squares & Rotated Shapers Laser - 0x014BB (Laser) - 0x0360E | 0x03317 159240 - 0x033BE (Pressure Plates 1 EP) - 0x033EA - True -159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 - True +159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 & 0x01BEA - True 159242 - 0x033DD (Pressure Plates 3 EP) - 0x01CD3 & 0x01CD5 - True 159243 - 0x033E5 (Pressure Plates 4 Left Exit EP) - 0x01D3F - True 159244 - 0x018B6 (Pressure Plates 4 Right Exit EP) - 0x01D3F - True diff --git a/worlds/witness/items.py b/worlds/witness/items.py index 15c693b25dd4..a8c889de937a 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/items.py @@ -115,6 +115,7 @@ def __init__(self, world: "WitnessWorld", logic: WitnessPlayerLogic, locat: Witn # Adjust item classifications based on game settings. eps_shuffled = self._world.options.shuffle_EPs come_to_you = self._world.options.elevators_come_to_you + difficulty = self._world.options.puzzle_randomization for item_name, item_data in self.item_data.items(): if not eps_shuffled and item_name in {"Monastery Garden Entry (Door)", "Monastery Shortcuts", @@ -130,10 +131,12 @@ def __init__(self, world: "WitnessWorld", logic: WitnessPlayerLogic, locat: Witn "Monastery Laser Shortcut (Door)", "Orchard Second Gate (Door)", "Jungle Bamboo Laser Shortcut (Door)", - "Keep Pressure Plates 2 Exit (Door)", "Caves Elevator Controls (Panel)"}: # Downgrade doors that don't gate progress. item_data.classification = ItemClassification.useful + elif item_name == "Keep Pressure Plates 2 Exit (Door)" and not (difficulty == "none" and eps_shuffled): + # PP2EP requires the door in vanilla puzzles, otherwise it's unnecessary + item_data.classification = ItemClassification.useful # Build the mandatory item list. self._mandatory_items: Dict[str, int] = {} From 81425641561cf2efc0e57d46e46367400c70e3eb Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sun, 10 Dec 2023 20:36:55 +0100 Subject: [PATCH 027/282] The Witness: Fix non-deterministic hints (#2514) --- worlds/witness/hints.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 1e54ec352cb6..d238aa4adfb6 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -161,7 +161,7 @@ ] -def get_always_hint_items(world: "WitnessWorld"): +def get_always_hint_items(world: "WitnessWorld") -> List[str]: always = [ "Boat", "Caves Shortcuts", @@ -187,17 +187,17 @@ def get_always_hint_items(world: "WitnessWorld"): return always -def get_always_hint_locations(_: "WitnessWorld"): - return { +def get_always_hint_locations(_: "WitnessWorld") -> List[str]: + return [ "Challenge Vault Box", "Mountain Bottom Floor Discard", "Theater Eclipse EP", "Shipwreck Couch EP", "Mountainside Cloud Cycle EP", - } + ] -def get_priority_hint_items(world: "WitnessWorld"): +def get_priority_hint_items(world: "WitnessWorld") -> List[str]: priority = { "Caves Mountain Shortcut (Door)", "Caves Swamp Shortcut (Door)", @@ -246,11 +246,11 @@ def get_priority_hint_items(world: "WitnessWorld"): lasers.append("Desert Laser") priority.update(world.random.sample(lasers, 6)) - return priority + return sorted(priority) -def get_priority_hint_locations(_: "WitnessWorld"): - return { +def get_priority_hint_locations(_: "WitnessWorld") -> List[str]: + return [ "Swamp Purple Underwater", "Shipwreck Vault Box", "Town RGB Room Left", @@ -264,7 +264,7 @@ def get_priority_hint_locations(_: "WitnessWorld"): "Tunnels Theater Flowers EP", "Boat Shipwreck Green EP", "Quarry Stoneworks Control Room Left", - } + ] def make_hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Item]): @@ -365,8 +365,8 @@ def make_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List[Item] remaining_hints = hint_amount - len(hints) priority_hint_amount = int(max(0.0, min(len(priority_hint_pairs) / 2, remaining_hints / 2))) - prog_items_in_this_world = sorted(list(prog_items_in_this_world)) - locations_in_this_world = sorted(list(loc_in_this_world)) + prog_items_in_this_world = sorted(prog_items_in_this_world) + locations_in_this_world = sorted(loc_in_this_world) world.random.shuffle(prog_items_in_this_world) world.random.shuffle(locations_in_this_world) From 1a05bad612040ba734e40f80539ae444afa07cd8 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 10 Dec 2023 20:38:49 +0100 Subject: [PATCH 028/282] Core: update modules (#2551) --- WebHostLib/requirements.txt | 2 +- requirements.txt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 654104252cec..62707d78cf1f 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -5,5 +5,5 @@ Flask-Caching>=2.1.0 Flask-Compress>=1.14 Flask-Limiter>=3.5.0 bokeh>=3.1.1; python_version <= '3.8' -bokeh>=3.2.2; python_version >= '3.9' +bokeh>=3.3.2; python_version >= '3.9' markupsafe>=2.1.3 diff --git a/requirements.txt b/requirements.txt index 7d93928bb5fc..0db55a803591 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ colorama>=0.4.5 -websockets>=11.0.3 +websockets>=12.0 PyYAML>=6.0.1 jellyfish>=1.0.3 jinja2>=3.1.2 schema>=0.7.5 -kivy>=2.2.0 +kivy>=2.2.1 bsdiff4>=1.2.4 platformdirs>=4.0.0 certifi>=2023.11.17 -cython>=3.0.5 +cython>=3.0.6 cymem>=2.0.8 orjson>=3.9.10 \ No newline at end of file From e8f96dabe80bfd7c3d89195e69aad3c3be906581 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 10 Dec 2023 20:42:07 +0100 Subject: [PATCH 029/282] Core: faster prog balance (#2586) * Core: rename world to multiworld in balance_multiworld_progression * Core: small optimization to progression balance speed --- Fill.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/Fill.py b/Fill.py index 342c155079dd..525d27d3388e 100644 --- a/Fill.py +++ b/Fill.py @@ -550,7 +550,7 @@ def flood_items(world: MultiWorld) -> None: break -def balance_multiworld_progression(world: MultiWorld) -> None: +def balance_multiworld_progression(multiworld: MultiWorld) -> None: # A system to reduce situations where players have no checks remaining, popularly known as "BK mode." # Overall progression balancing algorithm: # Gather up all locations in a sphere. @@ -558,28 +558,28 @@ def balance_multiworld_progression(world: MultiWorld) -> None: # If other players are below the threshold value, swap progression in this sphere into earlier spheres, # which gives more locations available by this sphere. balanceable_players: typing.Dict[int, float] = { - player: world.worlds[player].options.progression_balancing / 100 - for player in world.player_ids - if world.worlds[player].options.progression_balancing > 0 + player: multiworld.worlds[player].options.progression_balancing / 100 + for player in multiworld.player_ids + if multiworld.worlds[player].options.progression_balancing > 0 } if not balanceable_players: logging.info('Skipping multiworld progression balancing.') else: logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.') logging.debug(balanceable_players) - state: CollectionState = CollectionState(world) + state: CollectionState = CollectionState(multiworld) checked_locations: typing.Set[Location] = set() - unchecked_locations: typing.Set[Location] = set(world.get_locations()) + unchecked_locations: typing.Set[Location] = set(multiworld.get_locations()) total_locations_count: typing.Counter[int] = Counter( location.player - for location in world.get_locations() + for location in multiworld.get_locations() if not location.locked ) reachable_locations_count: typing.Dict[int, int] = { player: 0 - for player in world.player_ids - if total_locations_count[player] and len(world.get_filled_locations(player)) != 0 + for player in multiworld.player_ids + if total_locations_count[player] and len(multiworld.get_filled_locations(player)) != 0 } balanceable_players = { player: balanceable_players[player] @@ -658,7 +658,7 @@ def item_percentage(player: int, num: int) -> float: balancing_unchecked_locations.remove(location) if not location.locked: balancing_reachables[location.player] += 1 - if world.has_beaten_game(balancing_state) or all( + if multiworld.has_beaten_game(balancing_state) or all( item_percentage(player, reachables) >= threshold_percentages[player] for player, reachables in balancing_reachables.items() if player in threshold_percentages): @@ -675,7 +675,7 @@ def item_percentage(player: int, num: int) -> float: locations_to_test = unlocked_locations[player] items_to_test = list(candidate_items[player]) items_to_test.sort() - world.random.shuffle(items_to_test) + multiworld.random.shuffle(items_to_test) while items_to_test: testing = items_to_test.pop() reducing_state = state.copy() @@ -687,8 +687,8 @@ def item_percentage(player: int, num: int) -> float: reducing_state.sweep_for_events(locations=locations_to_test) - if world.has_beaten_game(balancing_state): - if not world.has_beaten_game(reducing_state): + if multiworld.has_beaten_game(balancing_state): + if not multiworld.has_beaten_game(reducing_state): items_to_replace.append(testing) else: reduced_sphere = get_sphere_locations(reducing_state, locations_to_test) @@ -696,33 +696,32 @@ def item_percentage(player: int, num: int) -> float: if p < threshold_percentages[player]: items_to_replace.append(testing) - replaced_items = False + old_moved_item_count = moved_item_count # sort then shuffle to maintain deterministic behaviour, # while allowing use of set for better algorithm growth behaviour elsewhere replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked) - world.random.shuffle(replacement_locations) + multiworld.random.shuffle(replacement_locations) items_to_replace.sort() - world.random.shuffle(items_to_replace) + multiworld.random.shuffle(items_to_replace) # Start swapping items. Since we swap into earlier spheres, no need for accessibility checks. while replacement_locations and items_to_replace: old_location = items_to_replace.pop() - for new_location in replacement_locations: + for i, new_location in enumerate(replacement_locations): if new_location.can_fill(state, old_location.item, False) and \ old_location.can_fill(state, new_location.item, False): - replacement_locations.remove(new_location) + replacement_locations.pop(i) swap_location_item(old_location, new_location) logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, " f"displacing {old_location.item} into {old_location}") moved_item_count += 1 state.collect(new_location.item, True, new_location) - replaced_items = True break else: logging.warning(f"Could not Progression Balance {old_location.item}") - if replaced_items: + if old_moved_item_count < moved_item_count: logging.debug(f"Moved {moved_item_count} items so far\n") unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]} for location in get_sphere_locations(state, unlocked): @@ -736,7 +735,7 @@ def item_percentage(player: int, num: int) -> float: state.collect(location.item, True, location) checked_locations |= sphere_locations - if world.has_beaten_game(state): + if multiworld.has_beaten_game(state): break elif not sphere_locations: logging.warning("Progression Balancing ran out of paths.") From 13122ab4668681772261195b88bf4c496152b55f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 10 Dec 2023 20:42:41 +0100 Subject: [PATCH 030/282] Core: remove start_inventory_from_pool from early_items (#2579) --- Main.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Main.py b/Main.py index b64650478bfe..8dac8f7d20eb 100644 --- a/Main.py +++ b/Main.py @@ -117,6 +117,17 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items(): for _ in range(count): world.push_precollected(world.create_item(item_name, player)) + # remove from_pool items also from early items handling, as starting is plenty early. + early = world.early_items[player].get(item_name, 0) + if early: + world.early_items[player][item_name] = max(0, early-count) + remaining_count = count-early + if remaining_count > 0: + local_early = world.early_local_items[player].get(item_name, 0) + if local_early: + world.early_items[player][item_name] = max(0, local_early - remaining_count) + del local_early + del early logger.info('Creating World.') AutoWorld.call_all(world, "create_regions") From d9d282c9259f79abc0e029d27075b45c3a631fbd Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Mon, 11 Dec 2023 19:14:44 -0600 Subject: [PATCH 031/282] Tests: test that the datapackage after generation is still valid (#2575) --- test/general/test_ids.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/test/general/test_ids.py b/test/general/test_ids.py index 4edfb8d994ef..98c41b67b176 100644 --- a/test/general/test_ids.py +++ b/test/general/test_ids.py @@ -1,5 +1,8 @@ import unittest -from worlds.AutoWorld import AutoWorldRegister + +from Fill import distribute_items_restrictive +from worlds.AutoWorld import AutoWorldRegister, call_all +from . import setup_solo_multiworld class TestIDs(unittest.TestCase): @@ -66,3 +69,34 @@ def test_duplicate_location_ids(self): for gamename, world_type in AutoWorldRegister.world_types.items(): with self.subTest(game=gamename): self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id)) + + def test_postgen_datapackage(self): + """Generates a solo multiworld and checks that the datapackage is still valid""" + for gamename, world_type in AutoWorldRegister.world_types.items(): + with self.subTest(game=gamename): + multiworld = setup_solo_multiworld(world_type) + distribute_items_restrictive(multiworld) + call_all(multiworld, "post_fill") + datapackage = world_type.get_data_package_data() + for item_group, item_names in datapackage["item_name_groups"].items(): + self.assertIsInstance(item_group, str, + f"item_name_group names should be strings: {item_group}") + for item_name in item_names: + self.assertIsInstance(item_name, str, + f"{item_name}, in group {item_group} is not a string") + for loc_group, loc_names in datapackage["location_name_groups"].items(): + self.assertIsInstance(loc_group, str, + f"location_name_group names should be strings: {loc_group}") + for loc_name in loc_names: + self.assertIsInstance(loc_name, str, + f"{loc_name}, in group {loc_group} is not a string") + for item_name, item_id in datapackage["item_name_to_id"].items(): + self.assertIsInstance(item_name, str, + f"{item_name} is not a valid item name for item_name_to_id") + self.assertIsInstance(item_id, int, + f"{item_id} for {item_name} should be an int") + for loc_name, loc_id in datapackage["location_name_to_id"].items(): + self.assertIsInstance(loc_name, str, + f"{loc_name} is not a valid item name for location_name_to_id") + self.assertIsInstance(loc_id, int, + f"{loc_id} for {loc_name} should be an int") From e9317d403176723cbf58584b8d597726acb0a6e8 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 11 Dec 2023 20:39:38 -0500 Subject: [PATCH 032/282] FFMQR: Fix Empty Kaeli Companion Event Location and Spellbook option (#2591) --- worlds/ffmq/Regions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/ffmq/Regions.py b/worlds/ffmq/Regions.py index aac8289a3600..61f70864c0b4 100644 --- a/worlds/ffmq/Regions.py +++ b/worlds/ffmq/Regions.py @@ -67,10 +67,10 @@ def create_regions(self): self.multiworld.regions.append(create_region(self.multiworld, self.player, room["name"], room["id"], [FFMQLocation(self.player, object["name"], location_table[object["name"]] if object["name"] in location_table else None, object["type"], object["access"], - self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for - object in room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in - ("BattlefieldGp", "BattlefieldXp") and (object["type"] != "Box" or - self.multiworld.brown_boxes[self.player] == "include")], room["links"])) + self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for object in + room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in ("BattlefieldGp", + "BattlefieldXp") and (object["type"] != "Box" or self.multiworld.brown_boxes[self.player] == "include") and + not (object["name"] == "Kaeli Companion" and not object["on_trigger"])], room["links"])) dark_king_room = self.multiworld.get_region("Doom Castle Dark King Room", self.player) dark_king = FFMQLocation(self.player, "Dark King", None, "Trigger", []) From 45fa9a8f9e6c28368968fced26878ddf02524636 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Mon, 11 Dec 2023 20:48:20 -0800 Subject: [PATCH 033/282] BizHawkClient: Add SGB to systems using explicit vblank callback (#2593) --- data/lua/connector_bizhawk_generic.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/lua/connector_bizhawk_generic.lua b/data/lua/connector_bizhawk_generic.lua index c4e729300dac..eff400cb032b 100644 --- a/data/lua/connector_bizhawk_generic.lua +++ b/data/lua/connector_bizhawk_generic.lua @@ -585,7 +585,7 @@ else -- misaligned, so for GB and GBC we explicitly set the callback on -- vblank instead. -- https://github.com/TASEmulators/BizHawk/issues/3711 - if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" then + if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" or emu.getsystemid() == "SGB" then event.onmemoryexecute(tick, 0x40, "tick", "System Bus") else event.onframeend(tick) From db1d195cb07244e0eceb365d2a6c9c5a6cc11c58 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 12 Dec 2023 20:11:10 -0600 Subject: [PATCH 034/282] Hollow Knight: remove unused option check (#2595) --- worlds/hk/__init__.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index f7e7e22e69dd..8b07b34eb0f2 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -419,17 +419,16 @@ def _compute_weights(weights: dict, desc: str) -> typing.Dict[str, int]: def set_rules(self): world = self.multiworld player = self.player - if world.logic[player] != 'nologic': - goal = world.Goal[player] - if goal == Goal.option_hollowknight: - world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) - elif goal == Goal.option_siblings: - world.completion_condition[player] = lambda state: state._hk_siblings_ending(player) - elif goal == Goal.option_radiance: - world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player) - else: - # Any goal - world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player) + goal = world.Goal[player] + if goal == Goal.option_hollowknight: + world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) + elif goal == Goal.option_siblings: + world.completion_condition[player] = lambda state: state._hk_siblings_ending(player) + elif goal == Goal.option_radiance: + world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player) + else: + # Any goal + world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player) set_rules(self) From 0eefe9e93646ef5f7915a4b8962d50f0fed8e54f Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Tue, 12 Dec 2023 20:12:16 -0600 Subject: [PATCH 035/282] WebHost: Some refactors and additional checks when uploading files. (#2549) --- WebHostLib/check.py | 59 +++++++++++++++++++++++--------------------- WebHostLib/upload.py | 58 +++++++++++++++++++++++++------------------ 2 files changed, 65 insertions(+), 52 deletions(-) diff --git a/WebHostLib/check.py b/WebHostLib/check.py index 4db2ec2ce35e..e739dda02d79 100644 --- a/WebHostLib/check.py +++ b/WebHostLib/check.py @@ -1,3 +1,4 @@ +import os import zipfile import base64 from typing import Union, Dict, Set, Tuple @@ -6,13 +7,7 @@ from markupsafe import Markup from WebHostLib import app - -banned_zip_contents = (".sfc",) - - -def allowed_file(filename): - return filename.endswith(('.txt', ".yaml", ".zip")) - +from WebHostLib.upload import allowed_options, allowed_options_extensions, banned_file from Generate import roll_settings, PlandoOptions from Utils import parse_yamls @@ -51,33 +46,41 @@ def mysterycheck(): def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]: options = {} for uploaded_file in files: - # if user does not select file, browser also - # submit an empty part without filename - if uploaded_file.filename == '': - return 'No selected file' + if banned_file(uploaded_file.filename): + return ("Uploaded data contained a rom file, which is likely to contain copyrighted material. " + "Your file was deleted.") + # If the user does not select file, the browser will still submit an empty string without a file name. + elif uploaded_file.filename == "": + return "No selected file." elif uploaded_file.filename in options: - return f'Conflicting files named {uploaded_file.filename} submitted' - elif uploaded_file and allowed_file(uploaded_file.filename): + return f"Conflicting files named {uploaded_file.filename} submitted." + elif uploaded_file and allowed_options(uploaded_file.filename): if uploaded_file.filename.endswith(".zip"): - - with zipfile.ZipFile(uploaded_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?') - - 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")): + if not zipfile.is_zipfile(uploaded_file): + return f"Uploaded file {uploaded_file.filename} is not a valid .zip file and cannot be opened." + + uploaded_file.seek(0) # offset from is_zipfile check + with zipfile.ZipFile(uploaded_file, "r") as zfile: + for file in zfile.infolist(): + # Remove folder pathing from str (e.g. "__MACOSX/" folder paths from archives created by macOS). + base_filename = os.path.basename(file.filename) + + if base_filename.endswith(".archipelago"): + return Markup("Error: Your .zip file contains an .archipelago file. " + 'Did you mean to host a game?') + elif base_filename.endswith(".zip"): + return "Nested .zip files inside a .zip are not supported." + elif banned_file(base_filename): + return ("Uploaded data contained a rom file, which is likely to contain copyrighted " + "material. Your file was deleted.") + # Ignore dot-files. + elif not base_filename.startswith(".") and allowed_options(base_filename): options[file.filename] = zfile.open(file, "r").read() else: options[uploaded_file.filename] = uploaded_file.read() + if not options: - return "Did not find a .yaml file to process." + return f"Did not find any valid files to process. Accepted formats: {allowed_options_extensions}" return options diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index e7ac033913e8..8f01294eac9a 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -19,7 +19,22 @@ from . import app from .models import Seed, Room, Slot, GameDataPackage -banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb") +banned_extensions = (".sfc", ".z64", ".n64", ".nes", ".smc", ".sms", ".gb", ".gbc", ".gba") +allowed_options_extensions = (".yaml", ".json", ".yml", ".txt", ".zip") +allowed_generation_extensions = (".archipelago", ".zip") + + +def allowed_options(filename: str) -> bool: + return filename.endswith(allowed_options_extensions) + + +def allowed_generation(filename: str) -> bool: + return filename.endswith(allowed_generation_extensions) + + +def banned_file(filename: str) -> bool: + return filename.endswith(banned_extensions) + def process_multidata(compressed_multidata, files={}): decompressed_multidata = MultiServer.Context.decompress(compressed_multidata) @@ -61,8 +76,8 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s if not owner: owner = session["_id"] infolist = zfile.infolist() - if all(file.filename.endswith((".yaml", ".yml")) or file.is_dir() for file in infolist): - flash(Markup("Error: Your .zip file only contains .yaml files. " + if all(allowed_options(file.filename) or file.is_dir() for file in infolist): + flash(Markup("Error: Your .zip file only contains options files. " 'Did you mean to generate a game?')) return @@ -73,7 +88,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s # Load files. for file in infolist: handler = AutoPatchRegister.get_handler(file.filename) - if file.filename.endswith(banned_zip_contents): + if banned_file(file.filename): return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \ "Your file was deleted." @@ -136,35 +151,34 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s flash("No multidata was found in the zip file, which is required.") -@app.route('/uploads', methods=['GET', 'POST']) +@app.route("/uploads", methods=["GET", "POST"]) def uploads(): - if request.method == 'POST': - # check if the post request has the file part - if 'file' not in request.files: - flash('No file part') + if request.method == "POST": + # check if the POST request has a file part. + if "file" not in request.files: + flash("No file part in POST request.") else: - file = request.files['file'] - # if user does not select file, browser also - # submit an empty part without filename - if file.filename == '': - flash('No selected file') - elif file and allowed_file(file.filename): - if zipfile.is_zipfile(file): - with zipfile.ZipFile(file, 'r') as zfile: + uploaded_file = request.files["file"] + # If the user does not select file, the browser will still submit an empty string without a file name. + if uploaded_file.filename == "": + flash("No selected file.") + elif uploaded_file and allowed_generation(uploaded_file.filename): + if zipfile.is_zipfile(uploaded_file): + with zipfile.ZipFile(uploaded_file, "r") as zfile: try: res = upload_zip_to_db(zfile) except VersionException: flash(f"Could not load multidata. Wrong Version detected.") else: - if type(res) == str: + if res is str: return res elif res: return redirect(url_for("view_seed", seed=res.id)) else: - file.seek(0) # offset from is_zipfile check + uploaded_file.seek(0) # offset from is_zipfile check # noinspection PyBroadException try: - multidata = file.read() + multidata = uploaded_file.read() slots, multidata = process_multidata(multidata) except Exception as e: flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})") @@ -182,7 +196,3 @@ def user_content(): rooms = select(room for room in Room if room.owner == session["_id"]) seeds = select(seed for seed in Seed if seed.owner == session["_id"]) return render_template("userContent.html", rooms=rooms, seeds=seeds) - - -def allowed_file(filename): - return filename.endswith(('.archipelago', ".zip")) From a3b0476b4baca0092c44ee5b275f0a0ac6eda7af Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Wed, 13 Dec 2023 17:34:36 -0500 Subject: [PATCH 036/282] LTTP: Boss rule fix (#2600) --- worlds/alttp/Rules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 469f4f82eefd..8a04f87afa02 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -136,7 +136,8 @@ def mirrorless_path_to_castle_courtyard(world, player): def set_defeat_dungeon_boss_rule(location): # Lambda required to defer evaluation of dungeon.boss since it will change later if boss shuffle is used - set_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state)) + add_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state)) + def set_always_allow(spot, rule): spot.always_allow = rule From ff556bf4ccd96931b0c7c29becedc2f562931cbe Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Wed, 13 Dec 2023 23:46:46 +0100 Subject: [PATCH 037/282] sm64ex: Fix server (#2599) --- worlds/sm64ex/Rules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index 5f85bcdafd68..c428f8554329 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -40,7 +40,8 @@ def set_rules(world, player: int, area_connections: dict): fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave"}, swapdict, world) # Destination Format: LVL | AREA with LVL = LEVEL_x, AREA = Area as used in sm64 code - area_connections.update({entrance_lvl: sm64_entrances_to_level[destination] for (entrance_lvl,destination) in randomized_entrances.items()}) + # Cast to int to not rely on availability of SM64Levels enum. Will cause crash in MultiServer otherwise + area_connections.update({int(entrance_lvl): int(sm64_entrances_to_level[destination]) for (entrance_lvl,destination) in randomized_entrances.items()}) randomized_entrances_s = {sm64_level_to_entrances[entrance_lvl]: destination for (entrance_lvl,destination) in randomized_entrances.items()} connect_regions(world, player, "Menu", randomized_entrances_s["Bob-omb Battlefield"]) From 3e3af385fa39c21ab62c7fb74a31d2491f9246ee Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Wed, 13 Dec 2023 17:57:14 -0500 Subject: [PATCH 038/282] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20client=20location?= =?UTF-8?q?s=20import=20(#2596)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/pokemon_rb/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_rb/client.py b/worlds/pokemon_rb/client.py index fb29045cf4e8..7424cc8ddff6 100644 --- a/worlds/pokemon_rb/client.py +++ b/worlds/pokemon_rb/client.py @@ -6,7 +6,7 @@ from worlds._bizhawk.client import BizHawkClient from worlds._bizhawk import read, write, guarded_write -from worlds.pokemon_rb.locations import location_data +from .locations import location_data logger = logging.getLogger("Client") From 394633558fae17683bb9baaa5f4d4da01fd0b57c Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri, 15 Dec 2023 14:39:09 -0500 Subject: [PATCH 039/282] ALTTP: Restore allow_excluded (#2607) Restores allow_excluded to the dungeon fill_restrictive call, which was apparently removed by mistake during merge conflict resolution --- worlds/alttp/Dungeons.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index a68acf7288cf..b456174f39b7 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -264,7 +264,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld): if loc in all_state_base.events: all_state_base.events.remove(loc) - fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, + fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True, name="LttP Dungeon Items") From b500cf600c329b7c6e598f928be3ff173ceea020 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri, 15 Dec 2023 22:16:13 -0500 Subject: [PATCH 040/282] FFMQ: Actually fix the spellbook option (#2594) --- worlds/ffmq/Options.py | 4 ++-- worlds/ffmq/data/settings.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/ffmq/Options.py b/worlds/ffmq/Options.py index eaf309749494..4b9f4a4a8819 100644 --- a/worlds/ffmq/Options.py +++ b/worlds/ffmq/Options.py @@ -267,11 +267,11 @@ class CompanionLevelingType(Choice): class CompanionSpellbookType(Choice): """Update companions' spellbook. Standard: Original game spellbooks. - Standard Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard. + Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard. Random Balanced: Randomize the spellbooks with an appropriate mix of spells. Random Chaos: Randomize the spellbooks in total free-for-all.""" option_standard = 0 - option_standard_extended = 1 + option_extended = 1 option_random_balanced = 2 option_random_chaos = 3 default = 0 diff --git a/worlds/ffmq/data/settings.yaml b/worlds/ffmq/data/settings.yaml index ff03ed26e63b..826a8c744d93 100644 --- a/worlds/ffmq/data/settings.yaml +++ b/worlds/ffmq/data/settings.yaml @@ -85,7 +85,7 @@ Final Fantasy Mystic Quest: Normal: 0 OneAndHalf: 0 Double: 0 - DoubleHalf: 0 + DoubleAndHalf: 0 Triple: 0 Quadruple: 0 companion_leveling_type: @@ -98,7 +98,7 @@ Final Fantasy Mystic Quest: BenPlus10: 0 companion_spellbook_type: Standard: 0 - StandardExtended: 0 + Extended: 0 RandomBalanced: 0 RandomChaos: 0 starting_companion: From 6c4fdc985df5d1fabe1bfeb3da594bda1b9baa5d Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Fri, 15 Dec 2023 22:16:36 -0500 Subject: [PATCH 041/282] SA2B: Fix Weapons Bed - Omochao 2 Logic (#2605) --- worlds/sa2b/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sa2b/Rules.py b/worlds/sa2b/Rules.py index 6b7ad69cd1a6..afd0dcb18226 100644 --- a/worlds/sa2b/Rules.py +++ b/worlds/sa2b/Rules.py @@ -638,7 +638,7 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla add_rule(multiworld.get_location(LocationName.radical_highway_omo_2, player), lambda state: state.has(ItemName.shadow_air_shoes, player)) add_rule(multiworld.get_location(LocationName.weapons_bed_omo_2, player), - lambda state: state.has(ItemName.eggman_jet_engine, player)) + lambda state: state.has(ItemName.eggman_large_cannon, player)) add_rule(multiworld.get_location(LocationName.mission_street_omo_3, player), lambda state: state.has(ItemName.tails_booster, player)) From c56cbd047488d09e3055d1247abb124269199502 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Fri, 15 Dec 2023 22:28:54 -0500 Subject: [PATCH 042/282] SM: item link replacement fix (#2597) --- worlds/sm/variaRandomizer/rando/GraphBuilder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/sm/variaRandomizer/rando/GraphBuilder.py b/worlds/sm/variaRandomizer/rando/GraphBuilder.py index 7bee33ec82e0..88b539e7f04a 100644 --- a/worlds/sm/variaRandomizer/rando/GraphBuilder.py +++ b/worlds/sm/variaRandomizer/rando/GraphBuilder.py @@ -170,7 +170,8 @@ def escapeTrigger(self, emptyContainer, graph, maxDiff, escapeTrigger): ap = "Landing Site" # dummy value it'll be overwritten at first collection while len(itemLocs) > 0 and not (sm.canPassG4() and graph.canAccess(sm, ap, "Landing Site", maxDiff)): il = itemLocs.pop(0) - if il.Location.restricted or il.Item.Type == "ArchipelagoItem": + # can happen with item links replacement items that its not in the container's itemPool + if il.Location.restricted or il.Item.Type == "ArchipelagoItem" or il.Item not in container.itemPool: continue self.log.debug("collecting " + getItemLocStr(il)) container.collect(il) From 7dff09dc1ad0d760621684c847169042ed78643f Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 16 Dec 2023 15:21:05 -0600 Subject: [PATCH 043/282] Options: set old options api before the world is created (#2378) --- BaseClasses.py | 15 ++++++++++----- Utils.py | 19 +++++++++++++++++++ worlds/AutoWorld.py | 4 ++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index c0a77708c016..dddcfc3a6fa9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -252,15 +252,20 @@ def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optio range(1, self.players + 1)} def set_options(self, args: Namespace) -> None: + # TODO - remove this section once all worlds use options dataclasses + all_keys: Set[str] = {key for player in self.player_ids for key in + AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints} + for option_key in all_keys: + option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. " + f"Please use `self.options.{option_key}` instead.") + option.update(getattr(args, option_key, {})) + setattr(self, option_key, option) + for player in self.player_ids: world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] self.worlds[player] = world_type(self, player) self.worlds[player].random = self.per_slot_randoms[player] - for option_key in world_type.options_dataclass.type_hints: - option_values = getattr(args, option_key, {}) - setattr(self, option_key, option_values) - # TODO - remove this loop once all worlds use options dataclasses - options_dataclass: typing.Type[Options.PerGameCommonOptions] = self.worlds[player].options_dataclass + options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player] for option_key in options_dataclass.type_hints}) diff --git a/Utils.py b/Utils.py index 5955e924322f..f6e4a9ab6052 100644 --- a/Utils.py +++ b/Utils.py @@ -779,6 +779,25 @@ def deprecate(message: str): import warnings warnings.warn(message) + +class DeprecateDict(dict): + log_message: str + should_error: bool + + def __init__(self, message, error: bool = False) -> None: + self.log_message = message + self.should_error = error + super().__init__() + + def __getitem__(self, item: Any) -> Any: + if self.should_error: + deprecate(self.log_message) + elif __debug__: + import warnings + warnings.warn(self.log_message) + return super().__getitem__(item) + + def _extend_freeze_support() -> None: """Extend multiprocessing.freeze_support() to also work on Non-Windows for spawn.""" # upstream issue: https://github.com/python/cpython/issues/76327 diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 5d0533e068d6..f56c39f69086 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -77,6 +77,10 @@ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Aut # create missing options_dataclass from legacy option_definitions # TODO - remove this once all worlds use options dataclasses if "options_dataclass" not in dct and "option_definitions" in dct: + # TODO - switch to deprecate after a version + if __debug__: + from warnings import warn + warn("Assigning options through option_definitions is now deprecated. Use options_dataclass instead.") dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(), bases=(PerGameCommonOptions,)) From f958af4067c9f0be0faf5349e19cb5a92b190edf Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sat, 16 Dec 2023 15:22:51 -0600 Subject: [PATCH 044/282] Adventure: Fix KeyError on Retrieved (#2560) --- AdventureClient.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/AdventureClient.py b/AdventureClient.py index d2f4e734ac2c..06e4d60dad43 100644 --- a/AdventureClient.py +++ b/AdventureClient.py @@ -115,11 +115,12 @@ def on_package(self, cmd: str, args: dict): msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}" self._set_message(msg, SYSTEM_MESSAGE_ID) elif cmd == "Retrieved": - self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"] - if self.freeincarnates_used is None: - self.freeincarnates_used = 0 - self.freeincarnates_used += self.freeincarnate_pending - self.send_pending_freeincarnates() + if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]: + self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"] + if self.freeincarnates_used is None: + self.freeincarnates_used = 0 + self.freeincarnates_used += self.freeincarnate_pending + self.send_pending_freeincarnates() elif cmd == "SetReply": if args["key"] == f"adventure_{self.auth}_freeincarnates_used": self.freeincarnates_used = args["value"] From 4979314825c4498bbaa16409ce1a4275c80bbf4b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 17 Dec 2023 06:08:40 +0100 Subject: [PATCH 045/282] Webhost: open graph support for /room (#2580) * WebHost: add Open Graph metadata to /room * WebHost: Open Graph cleanup --- WebHostLib/templates/hostRoom.html | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index ba15d64acac1..2981c41452f0 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -3,6 +3,16 @@ {% block head %} Multiworld {{ room.id|suuid }} {% if should_refresh %}{% endif %} + + + + {% if room.seed.slots|length < 2 %} + + {% else %} + + {% endif %} {% endblock %} From a549af8304de03c3a6814c53d589a7b2f4a9c3f4 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sun, 17 Dec 2023 10:11:40 -0600 Subject: [PATCH 046/282] Hollow Knight: Add additional DeathLink option and add ExtraPlatforms option. (#2545) --- worlds/hk/Options.py | 47 ++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index fcc938474d0c..ef7fbd0dfef6 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -2,7 +2,7 @@ from .ExtractedData import logic_options, starts, pool_options from .Rules import cost_terms -from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange +from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink from .Charms import vanilla_costs, names as charm_names if typing.TYPE_CHECKING: @@ -402,22 +402,34 @@ class WhitePalace(Choice): default = 0 -class DeathLink(Choice): - """ - When you die, everyone dies. Of course the reverse is true too. - When enabled, choose how incoming deathlinks are handled: - vanilla: DeathLink kills you and is just like any other death. RIP your previous shade and geo. - shadeless: DeathLink kills you, but no shade spawns and no geo is lost. Your previous shade, if any, is untouched. - shade: DeathLink functions like a normal death if you do not already have a shade, shadeless otherwise. +class ExtraPlatforms(DefaultOnToggle): + """Places additional platforms to make traveling throughout Hallownest more convenient.""" + + +class DeathLinkShade(Choice): + """Sets whether to create a shade when you are killed by a DeathLink and how to handle your existing shade, if any. + + vanilla: DeathLink deaths function like any other death and overrides your existing shade (including geo), if any. + shadeless: DeathLink deaths do not spawn shades. Your existing shade (including geo), if any, is untouched. + shade: DeathLink deaths spawn a shade if you do not have an existing shade. Otherwise, it acts like shadeless. + + * This option has no effect if DeathLink is disabled. + ** Self-death shade behavior is not changed; if a self-death normally creates a shade in vanilla, it will override + your existing shade, if any. """ - option_off = 0 - alias_no = 0 - alias_true = 1 - alias_on = 1 - alias_yes = 1 + option_vanilla = 0 option_shadeless = 1 - option_vanilla = 2 - option_shade = 3 + option_shade = 2 + default = 2 + + +class DeathLinkBreaksFragileCharms(Toggle): + """Sets if fragile charms break when you are killed by a DeathLink. + + * This option has no effect if DeathLink is disabled. + ** Self-death fragile charm behavior is not changed; if a self-death normally breaks fragile charms in vanilla, it + will continue to do so. + """ class StartingGeo(Range): @@ -476,7 +488,8 @@ class CostSanityHybridChance(Range): **{ option.__name__: option for option in ( - StartLocation, Goal, WhitePalace, StartingGeo, DeathLink, + StartLocation, Goal, WhitePalace, ExtraPlatforms, StartingGeo, + DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms, MinimumGeoPrice, MaximumGeoPrice, MinimumGrubPrice, MaximumGrubPrice, MinimumEssencePrice, MaximumEssencePrice, @@ -488,7 +501,7 @@ class CostSanityHybridChance(Range): LegEaterShopSlots, GrubfatherRewardSlots, SeerRewardSlots, ExtraShopSlots, SplitCrystalHeart, SplitMothwingCloak, SplitMantisClaw, - CostSanity, CostSanityHybridChance, + CostSanity, CostSanityHybridChance ) }, **cost_sanity_weights From c8adadb08bbd23d1d11c114333fdb95477eb4fbf Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:39:04 -0500 Subject: [PATCH 047/282] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Fix=20Flash=20lea?= =?UTF-8?q?rnable=20logic=20(#2615)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/pokemon_rb/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index d9bd6dde76e5..ee0f0052e1cf 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -414,7 +414,7 @@ def number_of_zones(mon): > 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")))): intervene_move = "Cut" elif ((not logic.can_learn_hm(test_state, "Flash", self.player)) and self.multiworld.dark_rock_tunnel_logic[self.player] - and (((self.multiworld.accessibility[self.player] != "minimal" and + and (((self.multiworld.accessibility[self.player] != "minimal" or (self.multiworld.trainersanity[self.player] or self.multiworld.extra_key_items[self.player])) or self.multiworld.door_shuffle[self.player]))): intervene_move = "Flash" From 817197c14dabf8a90cf81464ce4c7cc1fb477493 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Mon, 18 Dec 2023 10:46:24 -0500 Subject: [PATCH 048/282] Lingo: Tests no longer disable forced good item (#2602) The static class with the "disable forced good item" field is gone. Now, certain tests that want to check for specific access progression can run a method that removes the forced good item and adds it back to the pool. Tests that don't care about this will collect the forced good item like normal. This should prevent the intermittent fill failures on complex doors unit tests, since the forced good item should provide enough locations to fill in. --- worlds/lingo/__init__.py | 1 - worlds/lingo/player_logic.py | 3 +-- worlds/lingo/test/TestDoors.py | 10 ++++++++++ worlds/lingo/test/TestOrangeTower.py | 4 ++++ worlds/lingo/test/TestProgressive.py | 6 ++++++ worlds/lingo/test/__init__.py | 8 ++++++-- worlds/lingo/testing.py | 2 -- 7 files changed, 27 insertions(+), 7 deletions(-) delete mode 100644 worlds/lingo/testing.py diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index f22d344c8f51..088967445007 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -11,7 +11,6 @@ from .player_logic import LingoPlayerLogic from .regions import create_regions from .static_logic import Room, RoomEntrance -from .testing import LingoTestOptions class LingoWebWorld(WebWorld): diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index b046f1cfe3e2..fa497c59bd45 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -6,7 +6,6 @@ from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \ PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, RoomAndDoor, \ RoomAndPanel -from .testing import LingoTestOptions if TYPE_CHECKING: from . import LingoWorld @@ -224,7 +223,7 @@ def __init__(self, world: "LingoWorld"): "kind of logic error.") if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \ - and not early_color_hallways and LingoTestOptions.disable_forced_good_item is False: + and not early_color_hallways is False: # If shuffle doors is on, force a useful item onto the HI panel. This may not necessarily get you out of BK, # but the goal is to allow you to reach at least one more check. The non-painting ones are hardcoded right # now. We only allow the entrance to the Pilgrim Room if color shuffle is off, because otherwise there are diff --git a/worlds/lingo/test/TestDoors.py b/worlds/lingo/test/TestDoors.py index f496c5f5785a..49a0f9c49010 100644 --- a/worlds/lingo/test/TestDoors.py +++ b/worlds/lingo/test/TestDoors.py @@ -8,6 +8,8 @@ class TestRequiredRoomLogic(LingoTestBase): } def test_pilgrim_first(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Pilgrim Antechamber", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) @@ -28,6 +30,8 @@ def test_pilgrim_first(self) -> None: self.assertTrue(self.can_reach_location("The Seeker - Achievement")) def test_hidden_first(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) self.assertFalse(self.can_reach_location("The Seeker - Achievement")) @@ -55,6 +59,8 @@ class TestRequiredDoorLogic(LingoTestBase): } def test_through_rhyme(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) self.collect_by_name("Starting Room - Rhyme Room Entrance") @@ -64,6 +70,8 @@ def test_through_rhyme(self) -> None: self.assertTrue(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) def test_through_hidden(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) self.collect_by_name("Starting Room - Rhyme Room Entrance") @@ -83,6 +91,8 @@ class TestSimpleDoors(LingoTestBase): } def test_requirement(self): + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) diff --git a/worlds/lingo/test/TestOrangeTower.py b/worlds/lingo/test/TestOrangeTower.py index 7b0c3bb52518..9170de108ad0 100644 --- a/worlds/lingo/test/TestOrangeTower.py +++ b/worlds/lingo/test/TestOrangeTower.py @@ -8,6 +8,8 @@ class TestProgressiveOrangeTower(LingoTestBase): } def test_from_welcome_back(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) @@ -83,6 +85,8 @@ def test_from_welcome_back(self) -> None: self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) def test_from_hub_room(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) diff --git a/worlds/lingo/test/TestProgressive.py b/worlds/lingo/test/TestProgressive.py index 917c6e7e8939..8edc7ce6ccef 100644 --- a/worlds/lingo/test/TestProgressive.py +++ b/worlds/lingo/test/TestProgressive.py @@ -7,6 +7,8 @@ class TestComplexProgressiveHallwayRoom(LingoTestBase): } def test_item(self): + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) @@ -58,6 +60,8 @@ class TestSimpleHallwayRoom(LingoTestBase): } def test_item(self): + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) @@ -86,6 +90,8 @@ class TestProgressiveArtGallery(LingoTestBase): } def test_item(self): + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) diff --git a/worlds/lingo/test/__init__.py b/worlds/lingo/test/__init__.py index ffbf9032b64a..7ff456d8fcc3 100644 --- a/worlds/lingo/test/__init__.py +++ b/worlds/lingo/test/__init__.py @@ -1,7 +1,6 @@ from typing import ClassVar from test.bases import WorldTestBase -from .. import LingoTestOptions class LingoTestBase(WorldTestBase): @@ -9,5 +8,10 @@ class LingoTestBase(WorldTestBase): player: ClassVar[int] = 1 def world_setup(self, *args, **kwargs): - LingoTestOptions.disable_forced_good_item = True super().world_setup(*args, **kwargs) + + def remove_forced_good_item(self): + location = self.multiworld.get_location("Second Room - Good Luck", self.player) + self.remove(location.item) + self.multiworld.itempool.append(location.item) + self.multiworld.state.events.add(location) diff --git a/worlds/lingo/testing.py b/worlds/lingo/testing.py deleted file mode 100644 index 22fafea0fc6a..000000000000 --- a/worlds/lingo/testing.py +++ /dev/null @@ -1,2 +0,0 @@ -class LingoTestOptions: - disable_forced_good_item: bool = False From 8842f5d5c722529bab2fb3aa8187c3752dfe0c63 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 21 Dec 2023 04:11:11 +0100 Subject: [PATCH 049/282] Core: make update_reachable_regions local variables more wordy (#2522) --- BaseClasses.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index dddcfc3a6fa9..855e69c5d48c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -651,34 +651,34 @@ def __init__(self, parent: MultiWorld): def update_reachable_regions(self, player: int): self.stale[player] = False - rrp = self.reachable_regions[player] - bc = self.blocked_connections[player] + reachable_regions = self.reachable_regions[player] + blocked_connections = self.blocked_connections[player] queue = deque(self.blocked_connections[player]) - start = self.multiworld.get_region('Menu', player) + start = self.multiworld.get_region("Menu", player) # init on first call - this can't be done on construction since the regions don't exist yet - if start not in rrp: - rrp.add(start) - bc.update(start.exits) + if start not in reachable_regions: + reachable_regions.add(start) + blocked_connections.update(start.exits) queue.extend(start.exits) # run BFS on all connections, and keep track of those blocked by missing items while queue: connection = queue.popleft() new_region = connection.connected_region - if new_region in rrp: - bc.remove(connection) + if new_region in reachable_regions: + blocked_connections.remove(connection) elif connection.can_reach(self): assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region" - rrp.add(new_region) - bc.remove(connection) - bc.update(new_region.exits) + reachable_regions.add(new_region) + blocked_connections.remove(connection) + blocked_connections.update(new_region.exits) queue.extend(new_region.exits) self.path[new_region] = (new_region.name, self.path.get(connection, None)) # Retry connections if the new region can unblock them for new_entrance in self.multiworld.indirect_connections.get(new_region, set()): - if new_entrance in bc and new_entrance not in queue: + if new_entrance in blocked_connections and new_entrance not in queue: queue.append(new_entrance) def copy(self) -> CollectionState: From 0d929b81e8cfa19297e3acad16d0a220adefa487 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 21 Dec 2023 04:26:41 +0100 Subject: [PATCH 050/282] Factorio: fix files from mod base directory not being grabbed correctly in non-apworld (#2603) --- worlds/factorio/Mod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 21a8c684f939..d7b3d4b1ebca 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -177,7 +177,7 @@ def flop_random(low, high, base=None): else: basepath = os.path.join(os.path.dirname(__file__), "data", "mod") for dirpath, dirnames, filenames in os.walk(basepath): - base_arc_path = versioned_mod_name+"/"+os.path.relpath(dirpath, basepath) + base_arc_path = (versioned_mod_name+"/"+os.path.relpath(dirpath, basepath)).rstrip("/.\\") for filename in filenames: mod.writing_tasks.append(lambda arcpath=base_arc_path+"/"+filename, file_path=os.path.join(dirpath, filename): From bb0a0f2acaf04ab5919284a88f5cf613576d97d3 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 22 Dec 2023 20:02:49 -0800 Subject: [PATCH 051/282] Factorio: Fix unbeatable seeds where a science pack needs chemical plant (#2613) --- worlds/factorio/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index eb078720c668..17f3163e9026 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -246,7 +246,8 @@ def set_rules(self): location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \ (ingredient not in technology_table or state.has(ingredient, player)) and \ all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients - for technology in required_technologies[sub_ingredient]) + for technology in required_technologies[sub_ingredient]) and \ + all(state.has(technology.name, player) for technology in required_technologies[custom_recipe.crafting_machine]) else: location.access_rule = lambda state, ingredient=ingredient: \ all(state.has(technology.name, player) for technology in required_technologies[ingredient]) From 2512eb750109d83fe1003bf25d18002f09f39763 Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Wed, 27 Dec 2023 22:25:41 -0700 Subject: [PATCH 052/282] Hylics 2: Add missing logic (#2638) --- worlds/hylics2/Rules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/hylics2/Rules.py b/worlds/hylics2/Rules.py index 6c55c8745b17..ff9544e0e843 100644 --- a/worlds/hylics2/Rules.py +++ b/worlds/hylics2/Rules.py @@ -444,6 +444,8 @@ def set_rules(hylics2world): lambda state: paddle(state, player)) add_rule(world.get_location("Arcade 1: Alcove Medallion", player), lambda state: paddle(state, player)) + add_rule(world.get_location("Arcade 1: Lava Medallion", player), + lambda state: paddle(state, player)) add_rule(world.get_location("Foglast: Under Lair Medallion", player), lambda state: bridge_key(state, player)) add_rule(world.get_location("Foglast: Mid-Air Medallion", player), From 7c70b87f2971528730254638d2a30d61b9a8f851 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Thu, 28 Dec 2023 08:01:48 +0100 Subject: [PATCH 053/282] sm64ex: Fix randomizing Courses and Secrets separately (#2637) Backported from #2569 --- worlds/sm64ex/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index c428f8554329..fedd5b7a6ebd 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -21,7 +21,7 @@ def fix_reg(entrance_map: dict, entrance: SM64Levels, invalid_regions: set, def set_rules(world, player: int, area_connections: dict): randomized_level_to_paintings = sm64_level_to_paintings.copy() randomized_level_to_secrets = sm64_level_to_secrets.copy() - if world.AreaRandomizer[player].value == 1: # Some randomization is happening, randomize Courses + if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses randomized_level_to_paintings = shuffle_dict_keys(world,sm64_level_to_paintings) if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well randomized_level_to_secrets = shuffle_dict_keys(world,sm64_level_to_secrets) From b99c7349549b1a9597d13dcc25c264ab7ca3138c Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Thu, 28 Dec 2023 06:14:13 -0500 Subject: [PATCH 054/282] SM: strict rom validation fix (#2632) added a more robust ROM tag validation to free oher games to use tag starting with "SM" followed by another letter (SMW, SMZ3, SMRPG, SMMR,...) --- worlds/sm/Client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sm/Client.py b/worlds/sm/Client.py index df73ae47a0b6..756fd4bf36a7 100644 --- a/worlds/sm/Client.py +++ b/worlds/sm/Client.py @@ -61,7 +61,7 @@ async def validate_rom(self, ctx): from SNIClient import snes_buffered_write, snes_flush_writes, snes_read rom_name = await snes_read(ctx, SM_ROMNAME_START, ROMNAME_SIZE) - if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"SM" or rom_name[:3] == b"SMW": + if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"SM" or rom_name[2] not in b"1234567890": return False ctx.game = self.game From 576c7051068b3c99d13ff1b8afd7e7640ae4e51c Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 28 Dec 2023 06:15:48 -0500 Subject: [PATCH 055/282] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Badge=20plando=20?= =?UTF-8?q?fix=20(#2628)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only attempt to place badges in badge locations if they are empty. Return unplaced badges to the item pool if fewer than 8 locations are being filled. This should fix errors that occur when items are placed into badge locations via plando, or whatever other worlds may do. --- worlds/pokemon_rb/__init__.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index ee0f0052e1cf..5a94a8b5ff26 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -281,18 +281,20 @@ def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations self.multiworld.itempool.remove(badge) progitempool.remove(badge) for _ in range(5): - badgelocs = [self.multiworld.get_location(loc, self.player) for loc in [ - "Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize", - "Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize", - "Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize", - "Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"]] + badgelocs = [ + self.multiworld.get_location(loc, self.player) for loc in [ + "Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize", + "Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize", + "Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize", + "Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize" + ] if self.multiworld.get_location(loc, self.player).item is None] state = self.multiworld.get_all_state(False) self.multiworld.random.shuffle(badges) self.multiworld.random.shuffle(badgelocs) badgelocs_copy = badgelocs.copy() # allow_partial so that unplaced badges aren't lost, for debugging purposes fill_restrictive(self.multiworld, state, badgelocs_copy, badges, True, True, allow_partial=True) - if badges: + if len(badges) > 8 - len(badgelocs): for location in badgelocs: if location.item: badges.append(location.item) @@ -302,6 +304,7 @@ def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations for location in badgelocs: if location.item: fill_locations.remove(location) + progitempool += badges break else: raise FillError(f"Failed to place badges for player {self.player}") From 70eb2b58f55c277433d4f453ea2f5daa51462fbf Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Thu, 28 Dec 2023 06:16:38 -0500 Subject: [PATCH 056/282] [TLOZ] Fix bug with item drops in non-expanded item pool (#2623) There was a bug in non-expanded item pool where due to the base patch changes to accommodate more items in dungeons, some items were transformed into glitch items that removed bombs (this also happened in expanded item pool, but the item placement would overwrite the results of this bug so it didn't appear as frequently). Being a Zelda game, losing bombs is bad. This PR fixes the base patch process to avoid this bug, by properly carrying the value of a variable through a procedure. --- worlds/tloz/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 20ab003ead61..6e8927c4e7b9 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -200,15 +200,17 @@ def apply_base_patch(self, rom): for i in range(0, 0x7F): item = rom_data[first_quest_dungeon_items_early + i] if item & 0b00100000: - rom_data[first_quest_dungeon_items_early + i] = item & 0b11011111 - rom_data[first_quest_dungeon_items_early + i] = item | 0b01000000 + item = item & 0b11011111 + item = item | 0b01000000 + rom_data[first_quest_dungeon_items_early + i] = item if item & 0b00011111 == 0b00000011: # Change all Item 03s to Item 3F, the proper "nothing" rom_data[first_quest_dungeon_items_early + i] = item | 0b00111111 item = rom_data[first_quest_dungeon_items_late + i] if item & 0b00100000: - rom_data[first_quest_dungeon_items_late + i] = item & 0b11011111 - rom_data[first_quest_dungeon_items_late + i] = item | 0b01000000 + item = item & 0b11011111 + item = item | 0b01000000 + rom_data[first_quest_dungeon_items_late + i] = item if item & 0b00011111 == 0b00000011: rom_data[first_quest_dungeon_items_late + i] = item | 0b00111111 return rom_data From 04d194db741ee190d87348c282195e299dccfc74 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 28 Dec 2023 04:33:30 -0800 Subject: [PATCH 057/282] Pokemon Emerald: Change "settings" to "options" in docs (#2517) * Pokemon Emerald: Change "settings" to "options" in docs * Pokemon Emerald: Fix two more usages of "setting" instead of "option" * Pokemon Emerald: Minor rephrase in docs Co-authored-by: Aaron Wagener --------- Co-authored-by: Aaron Wagener --- worlds/pokemon_emerald/docs/en_Pokemon Emerald.md | 10 +++++----- worlds/pokemon_emerald/docs/setup_en.md | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md b/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md index 5d50c37ea95c..8b09b51b38d3 100644 --- a/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md +++ b/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md @@ -1,15 +1,15 @@ # Pokémon Emerald -## Where is the settings page? +## Where is the options page? -You can read through all the settings and generate a YAML [here](../player-settings). +You can read through all the options and generate a YAML [here](../player-options). ## What does randomization do to this game? This randomizer handles both item randomization and pokémon randomization. Badges, HMs, gifts from NPCs, and items on the ground can all be randomized. There are also many options for randomizing wild pokémon, starters, opponent pokémon, abilities, types, etc… You can even change a percentage of single battles into double battles. Check the -[settings page](../player-settings) for a more comprehensive list of what can be changed. +[options page](../player-options) for a more comprehensive list of what can be changed. ## What items and locations get randomized? @@ -28,7 +28,7 @@ randomizer. Here are some of the more important ones: - You can have both bikes simultaneously - You can run or bike (almost) anywhere - The Wally catching tutorial is skipped -- All text is instant, and with a setting it can be automatically progressed by holding A +- All text is instant and, with an option, can be automatically progressed by holding A - When a Repel runs out, you will be prompted to use another - Many more minor improvements… @@ -44,7 +44,7 @@ your inventory. ## When the player receives an item, what happens? You will only receive items while in the overworld and not during battles. Depending on your `Receive Item Messages` -setting, the received item will either be silently added to your bag or you will be shown a text box with the item's +option, the received item will either be silently added to your bag or you will be shown a text box with the item's name and the item will be added to your bag while a fanfare plays. ## Can I play offline? diff --git a/worlds/pokemon_emerald/docs/setup_en.md b/worlds/pokemon_emerald/docs/setup_en.md index 3c5c8c193aa9..e3f6d3c3013b 100644 --- a/worlds/pokemon_emerald/docs/setup_en.md +++ b/worlds/pokemon_emerald/docs/setup_en.md @@ -26,8 +26,8 @@ clear it. ## Generating and Patching a Game -1. Create your settings file (YAML). You can make one on the -[Pokémon Emerald settings page](../../../games/Pokemon%20Emerald/player-settings). +1. Create your options file (YAML). You can make one on the +[Pokémon Emerald options page](../../../games/Pokemon%20Emerald/player-options). 2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game). This will generate an output file for you. Your patch file will have the `.apemerald` file extension. 3. Open `ArchipelagoLauncher.exe` From af1f6e9113a80442dd91fcef0b22935c216633c6 Mon Sep 17 00:00:00 2001 From: TheLynk <44308308+TheLynk@users.noreply.github.com> Date: Thu, 28 Dec 2023 13:43:42 +0100 Subject: [PATCH 058/282] Oot : Update setup fr (#2394) * add new translation * Add translation for OOT Setup in french * Update setup_fr.md * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update setup_fr.md Fix treu to true * Update worlds/oot/docs/setup_fr.md Co-authored-by: Marech * Update OOT Init and Update Minecraft Init * Fix formatting errors * Fix wrong link in stardew valley randomizer setup guide Fix wrong link in stardew valley randomizer setup guide * Add new translation for Adventure and Archipidle in french Add new translation for Adventure and Archipidle in french * Add more store in setup page subnautica for more fairness Add more store in setup page subnautica for more fairness * tweak update merge #1685 for lua file tweak update merge #1685 for lua file * fix text fix text * fix wrong translation fix wrong translation * Yes it's better Yes it's better Co-authored-by: Fabian Dill * Update OOT Setup FR Update OOT Setup FR * Tweak Text Tweak Text --------- Co-authored-by: Ludovic Marechal Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Fabian Dill --- worlds/oot/docs/setup_fr.md | 428 ++++-------------------------------- 1 file changed, 38 insertions(+), 390 deletions(-) diff --git a/worlds/oot/docs/setup_fr.md b/worlds/oot/docs/setup_fr.md index 57099cdf2e01..f5915e18782f 100644 --- a/worlds/oot/docs/setup_fr.md +++ b/worlds/oot/docs/setup_fr.md @@ -1,422 +1,70 @@ -# Guide d'installation Archipelago pour Ocarina of Time +# Guide de configuration pour Ocarina of Time Archipelago ## Important -Comme nous utilisons BizHawk, ce guide ne s'applique qu'aux systèmes Windows et Linux. +Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windows et Linux. ## Logiciel requis -- BizHawk : [BizHawk sort de TASVideos] (https://tasvideos.org/BizHawk/ReleaseHistory) - - Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.7 est recommandée pour la stabilité. +- BizHawk : [Sorties BizHawk de TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) + - Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.7 est recommandée pour des raisons de stabilité. - Des instructions d'installation détaillées pour BizHawk peuvent être trouvées sur le lien ci-dessus. - - Les utilisateurs Windows doivent d'abord exécuter le programme d'installation prereq, qui peut également être trouvé sur le lien ci-dessus. + - Les utilisateurs Windows doivent d'abord exécuter le programme d'installation des prérequis, qui peut également être trouvé sur le lien ci-dessus. - Le client Archipelago intégré, qui peut être installé [ici](https://github.com/ArchipelagoMW/Archipelago/releases) - (sélectionnez `Ocarina of Time Client` lors de l'installation). + (sélectionnez « Ocarina of Time Client » lors de l'installation). - Une ROM Ocarina of Time v1.0. ## Configuration de BizHawk -Une fois BizHawk installé, ouvrez BizHawk et modifiez les paramètres suivants : - -- Allez dans Config > Personnaliser. Basculez vers l'onglet Avancé, puis basculez le Lua Core de "NLua+KopiLua" vers - "Interface Lua+Lua". Redémarrez ensuite BizHawk. Ceci est nécessaire pour que le script Lua fonctionne correctement. - **REMARQUE : Même si "Lua+LuaInterface" est déjà sélectionné, basculez entre les deux options et resélectionnez-le. Nouvelles installations** - ** des versions plus récentes de BizHawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais se chargent toujours ** - **"NLua+KopiLua" jusqu'à ce que cette étape soit terminée.** -- Sous Config > Personnaliser > Avancé, assurez-vous que la case pour AutoSaveRAM est cochée et cliquez sur le bouton 5s. - Cela réduit la possibilité de perdre des données de sauvegarde en cas de plantage de l'émulateur. -- Sous Config > Personnaliser, cochez les cases "Exécuter en arrière-plan" et "Accepter la saisie en arrière-plan". Cela vous permettra de - continuer à jouer en arrière-plan, même si une autre fenêtre est sélectionnée. -- Sous Config> Raccourcis clavier, de nombreux raccourcis clavier sont répertoriés, dont beaucoup sont liés aux touches communes du clavier. Vous voudrez probablement - désactiver la plupart d'entre eux, ce que vous pouvez faire rapidement en utilisant `Esc`. -- Si vous jouez avec une manette, lorsque vous liez les commandes, désactivez "P1 A Up", "P1 A Down", "P1 A Left" et "P1 A Right" - car ceux-ci interfèrent avec la visée s'ils sont liés. Définissez l'entrée directionnelle à l'aide de l'onglet Analogique à la place. -- Sous N64, activez "Utiliser l'emplacement d'extension". Ceci est nécessaire pour que les sauvegardes fonctionnent. +Une fois BizHawk installé, ouvrez EmuHawk et modifiez les paramètres suivants : + +- (≤ 2,8) Allez dans Config > Personnaliser. Passez à l'onglet Avancé, puis faites passer le Lua Core de "NLua+KopiLua" à + "Lua+LuaInterface". Puis redémarrez EmuHawk. Ceci est nécessaire pour que le script Lua fonctionne correctement. + **REMARQUE : Même si « Lua+LuaInterface » est déjà sélectionné, basculez entre les deux options et resélectionnez-la. Nouvelles installations** + **des versions plus récentes d'EmuHawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais ce pendant refait l'épate juste au dessus par précautions** +- Sous Config > Personnaliser > Avancé, assurez-vous que la case AutoSaveRAM est cochée et cliquez sur le bouton 5s. + Cela réduit la possibilité de perdre des données de sauvegarde en cas de crash de l'émulateur. +- Sous Config > Personnaliser, cochez les cases « Exécuter en arrière-plan » et « Accepter la saisie en arrière-plan ». Cela vous permettra continuez à jouer en arrière-plan, même si une autre fenêtre est sélectionnée. +- Sous Config > Hotkeys, de nombreux raccourcis clavier sont répertoriés, dont beaucoup sont liés aux touches communes du clavier. Vous voudrez probablement pour désactiver la plupart d'entre eux, ce que vous pouvez faire rapidement en utilisant « Esc ». +- Si vous jouez avec une manette, lorsque vous associez des commandes, désactivez "P1 A Up", "P1 A Down", "P1 A Left" et "P1 A Right". + car ceux-ci interfèrent avec la visée s’ils sont liés. Définissez plutôt l'entrée directionnelle à l'aide de l'onglet Analogique. +- Sous N64, activez "Utiliser le connecteur d'extension". Ceci est nécessaire pour que les états de sauvegarde fonctionnent. (Le menu N64 n'apparaît qu'après le chargement d'une ROM.) -Il est fortement recommandé d'associer les extensions de rom N64 (\*.n64, \*.z64) au BizHawk que nous venons d'installer. -Pour ce faire, nous devons simplement rechercher n'importe quelle rom N64 que nous possédons, faire un clic droit et sélectionner "Ouvrir avec ...", dépliez -la liste qui apparaît et sélectionnez l'option du bas "Rechercher une autre application", puis naviguez jusqu'au dossier BizHawk -et sélectionnez EmuHawk.exe. +Il est fortement recommandé d'associer les extensions de rom N64 (\*.n64, \*.z64) à l'EmuHawk que nous venons d'installer. +Pour ce faire, vous devez simplement rechercher n'importe quelle rom N64 que vous possédez, faire un clic droit et sélectionner "Ouvrir avec...", déplier la liste qui apparaît et sélectionnez l'option du bas "Rechercher une autre application", puis accédez au dossier BizHawk et sélectionnez EmuHawk.exe. -Un guide de configuration BizHawk alternatif ainsi que divers conseils de dépannage peuvent être trouvés +Un guide de configuration BizHawk alternatif ainsi que divers conseils de dépannage sont disponibles [ici](https://wiki.ootrandomizer.com/index.php?title=Bizhawk). -## Configuration de votre fichier YAML +## Créer un fichier de configuration (.yaml) -### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ? +### Qu'est-ce qu'un fichier de configuration et pourquoi en ai-je besoin ? -Votre fichier YAML contient un ensemble d'options de configuration qui fournissent au générateur des informations sur la façon dont il doit -générer votre jeu. Chaque joueur d'un multimonde fournira son propre fichier YAML. Cette configuration permet à chaque joueur de profiter -d'une expérience personnalisée à leur goût, et différents joueurs dans le même multimonde peuvent tous avoir des options différentes. +Consultez le guide sur la configuration d'un YAML de base lors de la configuration de l'archipel. +guide : [Guide de configuration de base de Multiworld](/tutorial/Archipelago/setup/en) -### Où puis-je obtenir un fichier YAML ? +### Où puis-je obtenir un fichier de configuration (.yaml) ? -Un yaml OoT de base ressemblera à ceci. Il y a beaucoup d'options cosmétiques qui ont été supprimées pour le plaisir de ce -tutoriel, si vous voulez voir une liste complète, téléchargez Archipelago depuis -la [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) et recherchez l'exemple de fichier dans -le dossier "Lecteurs". +La page Paramètres du lecteur sur le site Web vous permet de configurer vos paramètres personnels et d'exporter un fichier de configuration depuis eux. Page des paramètres du joueur : [Page des paramètres du joueur d'Ocarina of Time](/games/Ocarina%20of%20Time/player-settings) -``` yaml -description: Modèle par défaut d'Ocarina of Time # Utilisé pour décrire votre yaml. Utile si vous avez plusieurs fichiers -# Votre nom dans le jeu. Les espaces seront remplacés par des underscores et il y a une limite de 16 caractères -name: VotreNom -game: - Ocarina of Time: 1 -requires: - version: 0.1.7 # Version d'Archipelago requise pour que ce yaml fonctionne comme prévu. -# Options partagées prises en charge par tous les jeux : -accessibility: - items: 0 # Garantit que vous pourrez acquérir tous les articles, mais vous ne pourrez peut-être pas accéder à tous les emplacements - locations: 50 # Garantit que vous pourrez accéder à tous les emplacements, et donc à tous les articles - none: 0 # Garantit seulement que le jeu est battable. Vous ne pourrez peut-être pas accéder à tous les emplacements ou acquérir tous les objets -progression_balancing: # Un système pour réduire le BK, comme dans les périodes où vous ne pouvez rien faire, en déplaçant vos éléments dans une sphère d'accès antérieure - 0: 0 # Choisissez un nombre inférieur si cela ne vous dérange pas d'avoir un multimonde plus long, ou si vous pouvez glitch / faire du hors logique. - 25: 0 - 50: 50 # Faites en sorte que vous ayez probablement des choses à faire. - 99: 0 # Obtenez les éléments importants tôt et restez en tête de la progression. -Ocarina of Time: - logic_rules: # définit la logique utilisée pour le générateur. - glitchless: 50 - glitched: 0 - no_logic: 0 - logic_no_night_tokens_without_suns_song: # Les skulltulas nocturnes nécessiteront logiquement le Chant du soleil. - false: 50 - true: 0 - open_forest: # Définissez l'état de la forêt de Kokiri et du chemin vers l'arbre Mojo. - open: 50 - closed_deku: 0 - closed: 0 - open_kakariko: # Définit l'état de la porte du village de Kakariko. - open: 50 - zelda: 0 - closed: 0 - open_door_of_time: # Ouvre la Porte du Temps par défaut, sans le Chant du Temps. - false: 0 - true: 50 - zora_fountain: # Définit l'état du roi Zora, bloquant le chemin vers la fontaine de Zora. - open: 0 - adult: 0 - closed: 50 - gerudo_fortress: # Définit les conditions d'accès à la forteresse Gerudo. - normal: 0 - fast: 50 - open: 0 - bridge: # Définit les exigences pour le pont arc-en-ciel. - open: 0 - vanilla: 0 - stones: 0 - medallions: 50 - dungeons: 0 - tokens: 0 - trials: # Définit le nombre d'épreuves requises dans le Château de Ganon. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 50 # valeur minimale - 6: 0 # valeur maximale - random: 0 - random-low: 0 - random-higt: 0 - starting_age: # Choisissez l'âge auquel Link commencera. - child: 50 - adult: 0 - triforce_hunt: # Rassemblez des morceaux de la Triforce dispersés dans le monde entier pour terminer le jeu. - false: 50 - true: 0 - triforce_goal: # Nombre de pièces Triforce nécessaires pour terminer le jeu. Nombre total placé déterminé par le paramètre Item Pool. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 1: 0 # valeur minimale - 50: 0 # valeur maximale - random: 0 - random-low: 0 - random-higt: 0 - 20: 50 - bombchus_in_logic: # Les Bombchus sont correctement pris en compte dans la logique. Le premier pack trouvé aura 20 chus ; Kokiri Shop et Bazaar vendent des recharges ; bombchus ouvre Bombchu Bowling. - false: 50 - true: 0 - bridge_stones: # Définissez le nombre de pierres spirituelles requises pour le pont arc-en-ciel. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 3: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - bridge_medallions: # Définissez le nombre de médaillons requis pour le pont arc-en-ciel. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 6: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - bridge_rewards: # Définissez le nombre de récompenses de donjon requises pour le pont arc-en-ciel. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 9: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - bridge_tokens: # Définissez le nombre de jetons Gold Skulltula requis pour le pont arc-en-ciel. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 100: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - shuffle_mapcompass: # Contrôle où mélanger les cartes et boussoles des donjons. - remove: 0 - startwith: 50 - vanilla: 0 - dungeon: 0 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_smallkeys: # Contrôle où mélanger les petites clés de donjon. - remove: 0 - vanilla: 0 - dungeon: 50 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_hideoutkeys: # Contrôle où mélanger les petites clés de la Forteresse Gerudo. - vanilla: 50 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_bosskeys: # Contrôle où mélanger les clés du boss, à l'exception de la clé du boss du château de Ganon. - remove: 0 - vanilla: 0 - dungeon: 50 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_ganon_bosskey: # Contrôle où mélanger la clé du patron du château de Ganon. - remove: 50 - vanilla: 0 - dungeon: 0 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - on_lacs: 0 - enhance_map_compass: # La carte indique si un donjon est vanille ou MQ. La boussole indique quelle est la récompense du donjon. - false: 50 - true: 0 - lacs_condition: # Définissez les exigences pour la cinématique de la Flèche lumineuse dans le Temple du temps. - vanilla: 50 - stones: 0 - medallions: 0 - dungeons: 0 - tokens: 0 - lacs_stones: # Définissez le nombre de pierres spirituelles requises pour le LACS. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 3: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - lacs_medallions: # Définissez le nombre de médaillons requis pour LACS. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 6: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - lacs_rewards: # Définissez le nombre de récompenses de donjon requises pour LACS. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 9: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - lacs_tokens: # Définissez le nombre de jetons Gold Skulltula requis pour le LACS. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 100: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - shuffle_song_items: # Définit où les chansons peuvent apparaître. - song: 50 - dungeon: 0 - any: 0 - shopsanity: # Randomise le contenu de la boutique. Réglez sur "off" pour ne pas mélanger les magasins ; "0" mélange les magasins mais ne n'autorise pas les articles multimonde dans les magasins. - 0: 0 - 1: 0 - 2: 0 - 3: 0 - 4: 0 - random_value: 0 - off: 50 - tokensanity : # les récompenses en jetons des Skulltulas dorées sont mélangées dans la réserve. - off: 50 - dungeons: 0 - overworld: 0 - all: 0 - shuffle_scrubs: # Mélangez les articles vendus par Business Scrubs et fixez les prix. - off: 50 - low: 0 - regular: 0 - random_prices: 0 - shuffle_cows: # les vaches donnent des objets lorsque la chanson d'Epona est jouée. - false: 50 - true: 0 - shuffle_kokiri_sword: # Mélangez l'épée Kokiri dans la réserve d'objets. - false: 50 - true: 0 - shuffle_ocarinas: # Mélangez l'Ocarina des fées et l'Ocarina du temps dans la réserve d'objets. - false: 50 - true: 0 - shuffle_weird_egg: # Mélangez l'œuf bizarre de Malon au château d'Hyrule. - false: 50 - true: 0 - shuffle_gerudo_card: # Mélangez la carte de membre Gerudo dans la réserve d'objets. - false: 50 - true: 0 - shuffle_beans: # Ajoute un paquet de 10 haricots au pool d'objets et change le vendeur de haricots pour qu'il vende un objet pour 60 roupies. - false: 50 - true: 0 - shuffle_medigoron_carpet_salesman: # Mélangez les objets vendus par Medigoron et le vendeur de tapis Haunted Wasteland. - false: 50 - true: 0 - skip_child_zelda: # le jeu commence avec la lettre de Zelda, l'objet de la berceuse de Zelda et les événements pertinents déjà terminés. - false: 50 - true: 0 - no_escape_sequence: # Ignore la séquence d'effondrement de la tour entre les combats de Ganondorf et de Ganon. - false: 50 - true: 0 - no_guard_stealth: # Le vide sanitaire du château d'Hyrule passe directement à Zelda. - false: 50 - true: 0 - no_epona_race: # Epona peut toujours être invoquée avec Epona's Song. - false: 50 - true: 0 - skip_some_minigame_phases: # Dampe Race et Horseback Archery donnent les deux récompenses si la deuxième condition est remplie lors de la première tentative. - false: 50 - true: 0 - complete_mask_quest: # Tous les masques sont immédiatement disponibles à l'emprunt dans la boutique Happy Mask. - false: 50 - true: 0 - useful_cutscenes: # Réactive la cinématique Poe dans le Temple de la forêt, Darunia dans le Temple du feu et l'introduction de Twinrova. Surtout utile pour les pépins. - false: 50 - true: 0 - fast_chests: # Toutes les animations des coffres sont rapides. Si désactivé, les éléments principaux ont une animation lente. - false: 50 - true: 0 - free_scarecrow: # Sortir l'ocarina près d'un point d'épouvantail fait apparaître Pierre sans avoir besoin de la chanson. - false: 50 - true: 0 - fast_bunny_hood: # Bunny Hood vous permet de vous déplacer 1,5 fois plus vite comme dans Majora's Mask. - false: 50 - true: 0 - chicken_count: # Contrôle le nombre de Cuccos pour qu'Anju donne un objet en tant qu'enfant. - \# vous pouvez ajouter des valeurs supplémentaires entre le minimum et le maximum - 0: 0 # valeur minimale - 7: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - hints: # les pierres à potins peuvent donner des indices sur l'emplacement des objets. - none: 0 - mask: 0 - agony: 0 - always: 50 - hint_dist: # Choisissez la distribution d'astuces à utiliser. Affecte la fréquence des indices forts, quels éléments sont toujours indiqués, etc. - balanced: 50 - ddr: 0 - league: 0 - mw2: 0 - scrubs: 0 - strong: 0 - tournament: 0 - useless: 0 - very_strong: 0 - text_shuffle: # Randomise le texte dans le jeu pour un effet comique. - none: 50 - except_hints: 0 - complete: 0 - damage_multiplier: # contrôle la quantité de dégâts subis par Link. - half: 0 - normal: 50 - double: 0 - quadruple: 0 - ohko: 0 - no_collectible_hearts: # les cœurs ne tomberont pas des ennemis ou des objets. - false: 50 - true: 0 - starting_tod: # Changer l'heure de début de la journée. - default: 50 - sunrise: 0 - morning: 0 - noon: 0 - afternoon: 0 - sunset: 0 - evening: 0 - midnight: 0 - witching_hour: 0 - start_with_consumables: # Démarrez le jeu avec des Deku Sticks et des Deku Nuts pleins. - false: 50 - true: 0 - start_with_rupees: # Commencez avec un portefeuille plein. Les mises à niveau de portefeuille rempliront également votre portefeuille. - false: 50 - true: 0 - item_pool_value: # modifie le nombre d'objets disponibles dans le jeu. - plentiful: 0 - balanced: 50 - scarce: 0 - minimal: 0 - junk_ice_traps: # Ajoute des pièges à glace au pool d'objets. - off: 0 - normal: 50 - on: 0 - mayhem: 0 - onslaught: 0 - ice_trap_appearance: # modifie l'apparence des pièges à glace en tant qu'éléments autonomes. - major_only: 50 - junk_only: 0 - anything: 0 - logic_earliest_adult_trade: # premier élément pouvant apparaître dans la séquence d'échange pour adultes. - pocket_egg: 0 - pocket_cucco: 0 - cojiro: 0 - odd_mushroom: 0 - poachers_saw: 0 - broken_sword: 0 - prescription: 50 - eyeball_frog: 0 - eyedrops: 0 - claim_check: 0 - logic_latest_adult_trade: # Dernier élément pouvant apparaître dans la séquence d'échange pour adultes. - pocket_egg: 0 - pocket_cucco: 0 - cojiro: 0 - odd_mushroom: 0 - poachers_saw: 0 - broken_sword: 0 - prescription: 0 - eyeball_frog: 0 - eyedrops: 0 - claim_check: 50 +### Vérification de votre fichier de configuration -``` +Si vous souhaitez valider votre fichier de configuration pour vous assurer qu'il fonctionne, vous pouvez le faire sur la page YAML Validator. +YAML page du validateur : [page de validation YAML](/mysterycheck) -## Rejoindre une partie MultiWorld +## Rejoindre un jeu multimonde -### Obtenez votre fichier de correctif OOT +### Obtenez votre fichier OOT modifié -Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à l'hébergeur. Une fois que c'est Fini, -l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun -des dossiers. Votre fichier de données doit avoir une extension `.apz5`. +Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à celui qui l'héberge. Une fois cela fait, l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun des dossiers. Votre fichier de données doit avoir une extension « .apz5 ». -Double-cliquez sur votre fichier `.apz5` pour démarrer votre client et démarrer le processus de patch ROM. Une fois le processus terminé -(cela peut prendre un certain temps), le client et l'émulateur seront lancés automatiquement (si vous avez associé l'extension -à l'émulateur comme recommandé). +Double-cliquez sur votre fichier « .apz5 » pour démarrer votre client et démarrer le processus de correctif ROM. Une fois le processus terminé (cela peut prendre un certain temps), le client et l'émulateur seront automatiquement démarrés (si vous avez associé l'extension à l'émulateur comme recommandé). ### Connectez-vous au multiserveur -Une fois le client et l'émulateur démarrés, vous devez les connecter. Dans l'émulateur, cliquez sur "Outils" -menu et sélectionnez "Console Lua". Cliquez sur le bouton du dossier ou appuyez sur Ctrl+O pour ouvrir un script Lua. - -Accédez à votre dossier d'installation Archipelago et ouvrez `data/lua/connector_oot.lua`. +Une fois le client et l'émulateur démarrés, vous devez les connecter. Accédez à votre dossier d'installation Archipelago, puis vers `data/lua`, et faites glisser et déposez le script `connector_oot.lua` sur la fenêtre principale d'EmuHawk. (Vous pourrez plutôt ouvrir depuis la console Lua manuellement, cliquez sur `Script` 〉 `Open Script` et accédez à `connector_oot.lua` avec le sélecteur de fichiers.) -Pour connecter le client au multiserveur, mettez simplement `:` dans le champ de texte en haut et appuyez sur Entrée (si le -le serveur utilise un mot de passe, saisissez dans le champ de texte inférieur `/connect : [mot de passe]`) +Pour connecter le client au multiserveur, mettez simplement `:` dans le champ de texte en haut et appuyez sur Entrée (si le serveur utilise un mot de passe, tapez dans le champ de texte inférieur `/connect : [mot de passe]`) -Vous êtes maintenant prêt à commencer votre aventure à Hyrule. +Vous êtes maintenant prêt à commencer votre aventure dans Hyrule. \ No newline at end of file From 7af654e619bb39ee45544484fa14046264238e30 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 28 Dec 2023 13:57:41 +0100 Subject: [PATCH 059/282] WebHost: validate uploaded datapackage and calculate own checksum (#2639) --- WebHostLib/upload.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index 8f01294eac9a..af4ed264aacd 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -11,11 +11,14 @@ from markupsafe import Markup from pony.orm import commit, flush, select, rollback from pony.orm.core import TransactionIntegrityError +import schema import MultiServer from NetUtils import SlotType from Utils import VersionException, __version__ +from worlds import GamesPackage from worlds.Files import AutoPatchRegister +from worlds.AutoWorld import data_package_checksum from . import app from .models import Seed, Room, Slot, GameDataPackage @@ -23,6 +26,15 @@ allowed_options_extensions = (".yaml", ".json", ".yml", ".txt", ".zip") allowed_generation_extensions = (".archipelago", ".zip") +games_package_schema = schema.Schema({ + "item_name_groups": {str: [str]}, + "item_name_to_id": {str: int}, + "location_name_groups": {str: [str]}, + "location_name_to_id": {str: int}, + schema.Optional("checksum"): str, + schema.Optional("version"): int, +}) + def allowed_options(filename: str) -> bool: return filename.endswith(allowed_options_extensions) @@ -37,6 +49,8 @@ def banned_file(filename: str) -> bool: def process_multidata(compressed_multidata, files={}): + game_data: GamesPackage + decompressed_multidata = MultiServer.Context.decompress(compressed_multidata) slots: typing.Set[Slot] = set() @@ -45,11 +59,19 @@ def process_multidata(compressed_multidata, files={}): game_data_packages: typing.List[GameDataPackage] = [] for game, game_data in decompressed_multidata["datapackage"].items(): if game_data.get("checksum"): + original_checksum = game_data.pop("checksum") + game_data = games_package_schema.validate(game_data) + game_data = {key: value for key, value in sorted(game_data.items())} + game_data["checksum"] = data_package_checksum(game_data) game_data_package = GameDataPackage(checksum=game_data["checksum"], data=pickle.dumps(game_data)) + if original_checksum != game_data["checksum"]: + raise Exception(f"Original checksum {original_checksum} != " + f"calculated checksum {game_data['checksum']} " + f"for game {game}.") decompressed_multidata["datapackage"][game] = { "version": game_data.get("version", 0), - "checksum": game_data["checksum"] + "checksum": game_data["checksum"], } try: commit() # commit game data package @@ -64,14 +86,15 @@ def process_multidata(compressed_multidata, files={}): if slot_info.type == SlotType.group: continue slots.add(Slot(data=files.get(slot, None), - player_name=slot_info.name, - player_id=slot, - game=slot_info.game)) + player_name=slot_info.name, + player_id=slot, + game=slot_info.game)) flush() # commit slots compressed_multidata = compressed_multidata[0:1] + zlib.compress(pickle.dumps(decompressed_multidata), 9) return slots, compressed_multidata + def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None): if not owner: owner = session["_id"] From 8e708f829d143fb32771282c0de57bb2e7955dff Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 28 Dec 2023 14:12:37 +0100 Subject: [PATCH 060/282] The Witness: Fix an instance of multiworld.random being used (#2630) o_o --- worlds/witness/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index c2d2311c1537..6360c33aefb9 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -143,7 +143,7 @@ def create_regions(self): # Pick an early item to place on the tutorial gate. early_items = [item for item in self.items.get_early_items() if item in self.items.get_mandatory_items()] if early_items: - random_early_item = self.multiworld.random.choice(early_items) + random_early_item = self.random.choice(early_items) if self.options.puzzle_randomization == 1: # In Expert, only tag the item as early, rather than forcing it onto the gate. self.multiworld.local_early_items[self.player][random_early_item] = 1 From c7617f92dd299f0907872c7ceb167d194b5dec93 Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Thu, 28 Dec 2023 08:17:23 -0500 Subject: [PATCH 061/282] TLOZ: Try accounting for non_local_items with the pool of starting weapons (#2620) It was brought up that if you attempt to non_local any of the starting weapons, there is still a chance for it to get chosen as your starting weapon if you are on a StartingPosition value lower than very_dangerous. This fix will attempt to build the starting weapons list accounting for non_local items, but if all possible weapons have been set to non_local, force one of them to be your starting weapon anyway since the player is still expecting a starting weapon in their world if they have chosen one of the lower StartingPosition values. --- worlds/tloz/ItemPool.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py index 7773accd8d7c..456598edecef 100644 --- a/worlds/tloz/ItemPool.py +++ b/worlds/tloz/ItemPool.py @@ -93,7 +93,11 @@ def get_pool_core(world): # Starting Weapon start_weapon_locations = starting_weapon_locations.copy() - starting_weapon = random.choice(starting_weapons) + final_starting_weapons = [weapon for weapon in starting_weapons + if weapon not in world.multiworld.non_local_items[world.player]] + if not final_starting_weapons: + final_starting_weapons = starting_weapons + starting_weapon = random.choice(final_starting_weapons) if world.multiworld.StartingPosition[world.player] == StartingPosition.option_safe: placed_items[start_weapon_locations[0]] = starting_weapon elif world.multiworld.StartingPosition[world.player] in \ From 901201f675794d76d8f08232e1799246f7c9ea50 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Thu, 28 Dec 2023 08:21:54 -0500 Subject: [PATCH 062/282] Noita: Don't allow impossible slot names (#2608) * Noita: Add note about allowable slot names * Update character list * Update init to raise an exception if a yaml has bad characters * Slightly adjust exception message --- worlds/noita/__init__.py | 4 ++++ worlds/noita/docs/setup_en.md | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/worlds/noita/__init__.py b/worlds/noita/__init__.py index 499d202a64f4..792b90e3f551 100644 --- a/worlds/noita/__init__.py +++ b/worlds/noita/__init__.py @@ -35,6 +35,10 @@ class NoitaWorld(World): web = NoitaWeb() + def generate_early(self): + if not self.multiworld.get_player_name(self.player).isascii(): + raise Exception("Noita yaml's slot name has invalid character(s).") + # Returned items will be sent over to the client def fill_slot_data(self): return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions} diff --git a/worlds/noita/docs/setup_en.md b/worlds/noita/docs/setup_en.md index bd6a151432bd..b67e840bb94c 100644 --- a/worlds/noita/docs/setup_en.md +++ b/worlds/noita/docs/setup_en.md @@ -40,6 +40,8 @@ or try restarting your game. ### What is a YAML and why do I need one? You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn about why Archipelago uses YAML files and what they're for. +Please note that Noita only allows you to type certain characters for your slot name. +These characters are: `` !#$%&'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~<>|\/`` ### Where do I get a YAML? You can use the [game settings page for Noita](/games/Noita/player-settings) here on the Archipelago website to @@ -54,4 +56,4 @@ Place the unzipped pack in the `packs` folder. Then, open Poptracker and open th Click on the "AP" symbol at the top, then enter the desired address, slot name, and password. That's all you need for it. It will provide you with a quick reference to see which checks you've done and -which checks you still have left. \ No newline at end of file +which checks you still have left. From 24ac3de12586ce1cfb47e371b9fa7a91e3f3b933 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 28 Dec 2023 14:30:10 +0100 Subject: [PATCH 063/282] Factorio: "improve" default start items (#2588) Makes it less likely that people kill themselves via pollution and gives them some healing items they may not even know about. --- worlds/factorio/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index b72d57ad9bfe..3429ebbd4251 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -210,7 +210,7 @@ class RecipeIngredientsOffset(Range): class FactorioStartItems(OptionDict): """Mapping of Factorio internal item-name to amount granted on start.""" display_name = "Starting Items" - default = {"burner-mining-drill": 19, "stone-furnace": 19} + default = {"burner-mining-drill": 4, "stone-furnace": 4, "raw-fish": 50} class FactorioFreeSampleBlacklist(OptionSet): From d1a17a350d94428ce345da9f23032bb91cc76e3f Mon Sep 17 00:00:00 2001 From: Jarno Date: Thu, 28 Dec 2023 14:41:24 +0100 Subject: [PATCH 064/282] Docs: Add missing Get location_name_groups_* to network protocol (#2550) --- docs/network protocol.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/network protocol.md b/docs/network protocol.md index 199f96f48131..274b6e3716bc 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -380,12 +380,13 @@ Additional arguments sent in this package will also be added to the [Retrieved]( Some special keys exist with specific return data, all of them have the prefix `_read_`, so `hints_{team}_{slot}` is `_read_hints_{team}_{slot}`. -| Name | Type | Notes | -|------------------------------|-------------------------------|---------------------------------------------------| -| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. | -| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. | -| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. | -| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. | +| Name | Type | Notes | +|----------------------------------|-------------------------------|-------------------------------------------------------| +| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. | +| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. | +| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. | +| location_name_groups_{game_name} | dict\[str, list\[str\]\] | location_name_groups belonging to the requested game. | +| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. | ### Set Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package. From e674e37e08fed77fa724021a29eeaa7d92b25bfa Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Thu, 28 Dec 2023 17:43:16 -0500 Subject: [PATCH 065/282] SMZ3: optimized message queues (#2611) --- worlds/smz3/Client.py | 22 +++++++++++----------- worlds/smz3/__init__.py | 3 ++- worlds/smz3/data/zsm.ips | Bin 1470841 -> 1470841 bytes 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/worlds/smz3/Client.py b/worlds/smz3/Client.py index 687a43b00f5c..859cf234eb95 100644 --- a/worlds/smz3/Client.py +++ b/worlds/smz3/Client.py @@ -69,7 +69,7 @@ async def game_watcher(self, ctx): ctx.finished_game = True return - data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, 4) + data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, 4) if data is None: return @@ -77,14 +77,14 @@ async def game_watcher(self, ctx): recv_item = data[2] | (data[3] << 8) while (recv_index < recv_item): - item_address = recv_index * 8 - message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x700 + item_address, 8) - is_z3_item = ((message[5] & 0x80) != 0) - masked_part = (message[5] & 0x7F) if is_z3_item else message[5] - item_index = ((message[4] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0) + item_address = recv_index * 2 + message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xDA0 + item_address, 2) + is_z3_item = ((message[1] & 0x80) != 0) + masked_part = (message[1] & 0x7F) if is_z3_item else message[1] + item_index = ((message[0] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0) recv_index += 1 - snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) + snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) from .TotalSMZ3.Location import locations_start_id from . import convertLocSMZ3IDToAPID @@ -95,7 +95,7 @@ async def game_watcher(self, ctx): snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) - data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x600, 4) + data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD36, 4) if data is None: return @@ -106,10 +106,10 @@ async def game_watcher(self, ctx): item = ctx.items_received[item_out_ptr] item_id = item.item - items_start_id - player_id = item.player if item.player <= SMZ3_ROM_PLAYER_LIMIT else 0 - snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * 4, bytes([player_id & 0xFF, (player_id >> 8) & 0xFF, item_id & 0xFF, (item_id >> 8) & 0xFF])) + player_id = item.player if item.player < SMZ3_ROM_PLAYER_LIMIT else 0 + snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * 2, bytes([player_id, item_id])) item_out_ptr += 1 - snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x602, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) + snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD38, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), ctx.location_names[item.location], item_out_ptr, len(ctx.items_received))) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 2cc2ac97d952..39aa42c07ad7 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -80,7 +80,8 @@ class SMZ3World(World): locationNamesGT: Set[str] = {loc.Name for loc in GanonsTower(None, None).Locations} # first added for 0.2.6 - required_client_version = (0, 2, 6) + # optimized message queues for 0.4.4 + required_client_version = (0, 4, 4) def __init__(self, world: MultiWorld, player: int): self.rom_name_available_event = threading.Event() diff --git a/worlds/smz3/data/zsm.ips b/worlds/smz3/data/zsm.ips index fff36d95d15c96a9cc756c4fd0aa10ccda3e1751..87a4f924f1933fcf59493753c034192ef03a328c 100644 GIT binary patch delta 508 zcmezQDDvl{$O+3CKWtnvO`6Fece8^`77OFb&8ro5T;lp7_~TYgayzHem*(8h?YW;B zftU%1nSq!Eh*`Jier9WyVpQM$k(d2ABcuBCo&4;ojArfk`PqS(1Bf|+m}~oee(u$e z7+t4#f8ss^v}AhZXKq7phulRkUOWh3xbRV<>Q_d#$F22txr?|~&B(}@zaV!Jh$Gbx zB(gm?4jHUB%U#rOmAgm-s1C#hGW&r-K(2)VgJ|9M`=7au80$9(g4CV2$X)cI=#j$u zPm&B5QrA9Vx{$E;J@bX=rLiX&KJcAn`M`Bj@B`aP@efQ5?n?zuvIE(iK(_T#!3*YV z|4Du@vg-XO2~^6|pnFIGWFXh7{R|0}CyxW8(DYbNo@#BF z%ast;9fnzVP(cYC1Q6@4Z2!l}qn!#2XRUmm?OOS~-_#kk+ZCtt0x{oq#p(PkeT+NX h)l>zxtEmcJ?O+sY|Fc#Qh(WY45Q}X8vsSeBFaS*}+?xOZ delta 513 zcmezQDDvl{$O+3CUvFG7O`2&z+GYotEEdKkn^!CBxWx5d@W-u~LNMw6(95PtXkhZ9wC2f%gPzQ($WcCAvfLsd!2GPpxS3h$bG1jjZ1gSgE zl(y(Y?jwcupClPBB&>bHbRl}}d*%yaOJh$meBe9D@`3B5;0Lyo;vbkAoR$ilWCyZ2 zfo!v-f)|X|{*(NmYt{Qt5~!4^LF14D$Uvau_A?|bYOUMP1SD$|PJykF0y&_8btM;2 zHOMPU4AZ;6a&PB$6aJz3pW%bZs{PBR`+nor=axefX#d9T!?t From 3d1be0c468d0717fb9781886b006b498fa7751a4 Mon Sep 17 00:00:00 2001 From: wildham <64616385+wildham0@users.noreply.github.com> Date: Mon, 1 Jan 2024 12:13:35 -0500 Subject: [PATCH 066/282] FF1: Fix terminated_event access_rule not getting set (#2648) --- worlds/ff1/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index 16905cc6da0c..4ff361c07243 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -74,6 +74,7 @@ def create_regions(self): items = get_options(self.multiworld, 'items', self.player) goal_rule = generate_rule([[name for name in items.keys() if name in FF1_PROGRESSION_LIST and name != "Shard"]], self.player) + terminated_event.access_rule = goal_rule if "Shard" in items.keys(): def goal_rule_and_shards(state): return goal_rule(state) and state.has("Shard", self.player, 32) From c104e81145d6a5e89a25fb90c402abe2585ce007 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Mon, 1 Jan 2024 11:42:41 -0800 Subject: [PATCH 067/282] Zillion: move client to worlds/zillion (#2649) --- ZillionClient.py | 505 +-------------------------------------- worlds/zillion/client.py | 501 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 506 insertions(+), 500 deletions(-) create mode 100644 worlds/zillion/client.py diff --git a/ZillionClient.py b/ZillionClient.py index 5f3cbb943faa..ef96edab045e 100644 --- a/ZillionClient.py +++ b/ZillionClient.py @@ -1,505 +1,10 @@ -import asyncio -import base64 -import platform -from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast +import ModuleUpdate +ModuleUpdate.update() -# CommonClient import first to trigger ModuleUpdater -from CommonClient import CommonContext, server_loop, gui_enabled, \ - ClientCommandProcessor, logger, get_base_parser -from NetUtils import ClientStatus -import Utils -from Utils import async_start - -import colorama - -from zilliandomizer.zri.memory import Memory -from zilliandomizer.zri import events -from zilliandomizer.utils.loc_name_maps import id_to_loc -from zilliandomizer.options import Chars -from zilliandomizer.patch import RescueInfo - -from worlds.zillion.id_maps import make_id_to_others -from worlds.zillion.config import base_id, zillion_map - - -class ZillionCommandProcessor(ClientCommandProcessor): - ctx: "ZillionContext" - - def _cmd_sms(self) -> None: - """ Tell the client that Zillion is running in RetroArch. """ - logger.info("ready to look for game") - self.ctx.look_for_retroarch.set() - - def _cmd_map(self) -> None: - """ Toggle view of the map tracker. """ - self.ctx.ui_toggle_map() - - -class ToggleCallback(Protocol): - def __call__(self) -> None: ... - - -class SetRoomCallback(Protocol): - def __call__(self, rooms: List[List[int]]) -> None: ... - - -class ZillionContext(CommonContext): - game = "Zillion" - command_processor = ZillionCommandProcessor - items_handling = 1 # receive items from other players - - known_name: Optional[str] - """ This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """ - - from_game: "asyncio.Queue[events.EventFromGame]" - to_game: "asyncio.Queue[events.EventToGame]" - ap_local_count: int - """ local checks watched by server """ - next_item: int - """ index in `items_received` """ - ap_id_to_name: Dict[int, str] - ap_id_to_zz_id: Dict[int, int] - start_char: Chars = "JJ" - rescues: Dict[int, RescueInfo] = {} - loc_mem_to_id: Dict[int, int] = {} - got_room_info: asyncio.Event - """ flag for connected to server """ - got_slot_data: asyncio.Event - """ serves as a flag for whether I am logged in to the server """ - - look_for_retroarch: asyncio.Event - """ - There is a bug in Python in Windows - https://github.com/python/cpython/issues/91227 - that makes it so if I look for RetroArch before it's ready, - it breaks the asyncio udp transport system. - - As a workaround, we don't look for RetroArch until this event is set. - """ - - ui_toggle_map: ToggleCallback - ui_set_rooms: SetRoomCallback - """ parameter is y 16 x 8 numbers to show in each room """ - - def __init__(self, - server_address: str, - password: str) -> None: - super().__init__(server_address, password) - self.known_name = None - self.from_game = asyncio.Queue() - self.to_game = asyncio.Queue() - self.got_room_info = asyncio.Event() - self.got_slot_data = asyncio.Event() - self.ui_toggle_map = lambda: None - self.ui_set_rooms = lambda rooms: None - - self.look_for_retroarch = asyncio.Event() - if platform.system() != "Windows": - # asyncio udp bug is only on Windows - self.look_for_retroarch.set() - - self.reset_game_state() - - def reset_game_state(self) -> None: - for _ in range(self.from_game.qsize()): - self.from_game.get_nowait() - for _ in range(self.to_game.qsize()): - self.to_game.get_nowait() - self.got_slot_data.clear() - - self.ap_local_count = 0 - self.next_item = 0 - self.ap_id_to_name = {} - self.ap_id_to_zz_id = {} - self.rescues = {} - self.loc_mem_to_id = {} - - self.locations_checked.clear() - self.missing_locations.clear() - self.checked_locations.clear() - self.finished_game = False - self.items_received.clear() - - # override - def on_deathlink(self, data: Dict[str, Any]) -> None: - self.to_game.put_nowait(events.DeathEventToGame()) - return super().on_deathlink(data) - - # override - async def server_auth(self, password_requested: bool = False) -> None: - if password_requested and not self.password: - await super().server_auth(password_requested) - if not self.auth: - logger.info('waiting for connection to game...') - return - logger.info("logging in to server...") - await self.send_connect() - - # override - def run_gui(self) -> None: - from kvui import GameManager - from kivy.core.text import Label as CoreLabel - from kivy.graphics import Ellipse, Color, Rectangle - from kivy.uix.layout import Layout - from kivy.uix.widget import Widget - - class ZillionManager(GameManager): - logging_pairs = [ - ("Client", "Archipelago") - ] - base_title = "Archipelago Zillion Client" - - class MapPanel(Widget): - MAP_WIDTH: ClassVar[int] = 281 - - _number_textures: List[Any] = [] - rooms: List[List[int]] = [] - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - - self.rooms = [[0 for _ in range(8)] for _ in range(16)] - - self._make_numbers() - self.update_map() - - self.bind(pos=self.update_map) - # self.bind(size=self.update_bg) - - def _make_numbers(self) -> None: - self._number_textures = [] - for n in range(10): - label = CoreLabel(text=str(n), font_size=22, color=(0.1, 0.9, 0, 1)) - label.refresh() - self._number_textures.append(label.texture) - - def update_map(self, *args: Any) -> None: - self.canvas.clear() - - with self.canvas: - Color(1, 1, 1, 1) - Rectangle(source=zillion_map, - pos=self.pos, - size=(ZillionManager.MapPanel.MAP_WIDTH, - int(ZillionManager.MapPanel.MAP_WIDTH * 1.456))) # aspect ratio of that image - for y in range(16): - for x in range(8): - num = self.rooms[15 - y][x] - if num > 0: - Color(0, 0, 0, 0.4) - pos = [self.pos[0] + 17 + x * 32, self.pos[1] + 14 + y * 24] - Ellipse(size=[22, 22], pos=pos) - Color(1, 1, 1, 1) - pos = [self.pos[0] + 22 + x * 32, self.pos[1] + 12 + y * 24] - num_texture = self._number_textures[num] - Rectangle(texture=num_texture, size=num_texture.size, pos=pos) - - def build(self) -> Layout: - container = super().build() - self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=0) - self.main_area_container.add_widget(self.map_widget) - return container - - def toggle_map_width(self) -> None: - if self.map_widget.width == 0: - self.map_widget.width = ZillionManager.MapPanel.MAP_WIDTH - else: - self.map_widget.width = 0 - self.container.do_layout() - - def set_rooms(self, rooms: List[List[int]]) -> None: - self.map_widget.rooms = rooms - self.map_widget.update_map() - - self.ui = ZillionManager(self) - self.ui_toggle_map = lambda: self.ui.toggle_map_width() - self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms) - run_co: Coroutine[Any, Any, None] = self.ui.async_run() - self.ui_task = asyncio.create_task(run_co, name="UI") - - def on_package(self, cmd: str, args: Dict[str, Any]) -> None: - self.room_item_numbers_to_ui() - if cmd == "Connected": - logger.info("logged in to Archipelago server") - if "slot_data" not in args: - logger.warn("`Connected` packet missing `slot_data`") - return - slot_data = args["slot_data"] - - if "start_char" not in slot_data: - logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `start_char`") - return - self.start_char = slot_data['start_char'] - if self.start_char not in {"Apple", "Champ", "JJ"}: - logger.warn("invalid Zillion `Connected` packet, " - f"`slot_data` `start_char` has invalid value: {self.start_char}") - - if "rescues" not in slot_data: - logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `rescues`") - return - rescues = slot_data["rescues"] - self.rescues = {} - for rescue_id, json_info in rescues.items(): - assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}" - # TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch? - assert json_info["start_char"] == self.start_char, \ - f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}' - ri = RescueInfo(json_info["start_char"], - json_info["room_code"], - json_info["mask"]) - self.rescues[0 if rescue_id == "0" else 1] = ri - - if "loc_mem_to_id" not in slot_data: - logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`") - return - loc_mem_to_id = slot_data["loc_mem_to_id"] - self.loc_mem_to_id = {} - for mem_str, id_str in loc_mem_to_id.items(): - mem = int(mem_str) - id_ = int(id_str) - room_i = mem // 256 - assert 0 <= room_i < 74 - assert id_ in id_to_loc - self.loc_mem_to_id[mem] = id_ - - if len(self.loc_mem_to_id) != 394: - logger.warn("invalid Zillion `Connected` packet, " - f"`slot_data` missing locations in `loc_mem_to_id` - len {len(self.loc_mem_to_id)}") - - self.got_slot_data.set() - - payload = { - "cmd": "Get", - "keys": [f"zillion-{self.auth}-doors"] - } - async_start(self.send_msgs([payload])) - elif cmd == "Retrieved": - if "keys" not in args: - logger.warning(f"invalid Retrieved packet to ZillionClient: {args}") - return - keys = cast(Dict[str, Optional[str]], args["keys"]) - doors_b64 = keys.get(f"zillion-{self.auth}-doors", None) - if doors_b64: - logger.info("received door data from server") - doors = base64.b64decode(doors_b64) - self.to_game.put_nowait(events.DoorEventToGame(doors)) - elif cmd == "RoomInfo": - self.seed_name = args["seed_name"] - self.got_room_info.set() - - def room_item_numbers_to_ui(self) -> None: - rooms = [[0 for _ in range(8)] for _ in range(16)] - for loc_id in self.missing_locations: - loc_id_small = loc_id - base_id - loc_name = id_to_loc[loc_id_small] - y = ord(loc_name[0]) - 65 - x = ord(loc_name[2]) - 49 - if y == 9 and x == 5: - # don't show main computer in numbers - continue - assert (0 <= y < 16) and (0 <= x < 8), f"invalid index from location name {loc_name}" - rooms[y][x] += 1 - # TODO: also add locations with locals lost from loading save state or reset - self.ui_set_rooms(rooms) - - def process_from_game_queue(self) -> None: - if self.from_game.qsize(): - event_from_game = self.from_game.get_nowait() - if isinstance(event_from_game, events.AcquireLocationEventFromGame): - server_id = event_from_game.id + base_id - loc_name = id_to_loc[event_from_game.id] - self.locations_checked.add(server_id) - if server_id in self.missing_locations: - self.ap_local_count += 1 - n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win - logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})') - async_start(self.send_msgs([ - {"cmd": 'LocationChecks', "locations": [server_id]} - ])) - else: - # This will happen a lot in Zillion, - # because all the key words are local and unwatched by the server. - logger.debug(f"DEBUG: {loc_name} not in missing") - elif isinstance(event_from_game, events.DeathEventFromGame): - async_start(self.send_death()) - elif isinstance(event_from_game, events.WinEventFromGame): - if not self.finished_game: - async_start(self.send_msgs([ - {"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL} - ])) - self.finished_game = True - elif isinstance(event_from_game, events.DoorEventFromGame): - if self.auth: - doors_b64 = base64.b64encode(event_from_game.doors).decode() - payload = { - "cmd": "Set", - "key": f"zillion-{self.auth}-doors", - "operations": [{"operation": "replace", "value": doors_b64}] - } - async_start(self.send_msgs([payload])) - else: - logger.warning(f"WARNING: unhandled event from game {event_from_game}") - - def process_items_received(self) -> None: - if len(self.items_received) > self.next_item: - zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received] - for index in range(self.next_item, len(self.items_received)): - ap_id = self.items_received[index].item - from_name = self.player_names[self.items_received[index].player] - # TODO: colors in this text, like sni client? - logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}') - self.to_game.put_nowait( - events.ItemEventToGame(zz_item_ids) - ) - self.next_item = len(self.items_received) - - -def name_seed_from_ram(data: bytes) -> Tuple[str, str]: - """ returns player name, and end of seed string """ - if len(data) == 0: - # no connection to game - return "", "xxx" - null_index = data.find(b'\x00') - if null_index == -1: - logger.warning(f"invalid game id in rom {repr(data)}") - null_index = len(data) - name = data[:null_index].decode() - null_index_2 = data.find(b'\x00', null_index + 1) - if null_index_2 == -1: - null_index_2 = len(data) - seed_name = data[null_index + 1:null_index_2].decode() - - return name, seed_name - - -async def zillion_sync_task(ctx: ZillionContext) -> None: - logger.info("started zillion sync task") - - # to work around the Python bug where we can't check for RetroArch - if not ctx.look_for_retroarch.is_set(): - logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.") - await asyncio.wait(( - asyncio.create_task(ctx.look_for_retroarch.wait()), - asyncio.create_task(ctx.exit_event.wait()) - ), return_when=asyncio.FIRST_COMPLETED) - - last_log = "" - - def log_no_spam(msg: str) -> None: - nonlocal last_log - if msg != last_log: - last_log = msg - logger.info(msg) - - # to only show this message once per client run - help_message_shown = False - - with Memory(ctx.from_game, ctx.to_game) as memory: - while not ctx.exit_event.is_set(): - ram = await memory.read() - game_id = memory.get_rom_to_ram_data(ram) - name, seed_end = name_seed_from_ram(game_id) - if len(name): - if name == ctx.known_name: - ctx.auth = name - # this is the name we know - if ctx.server and ctx.server.socket: # type: ignore - if ctx.got_room_info.is_set(): - if ctx.seed_name and ctx.seed_name.endswith(seed_end): - # correct seed - if memory.have_generation_info(): - log_no_spam("everything connected") - await memory.process_ram(ram) - ctx.process_from_game_queue() - ctx.process_items_received() - else: # no generation info - if ctx.got_slot_data.is_set(): - memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id) - ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \ - make_id_to_others(ctx.start_char) - ctx.next_item = 0 - ctx.ap_local_count = len(ctx.checked_locations) - else: # no slot data yet - async_start(ctx.send_connect()) - log_no_spam("logging in to server...") - await asyncio.wait(( - asyncio.create_task(ctx.got_slot_data.wait()), - asyncio.create_task(ctx.exit_event.wait()), - asyncio.create_task(asyncio.sleep(6)) - ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets - else: # not correct seed name - log_no_spam("incorrect seed - did you mix up roms?") - else: # no room info - # If we get here, it looks like `RoomInfo` packet got lost - log_no_spam("waiting for room info from server...") - else: # server not connected - log_no_spam("waiting for server connection...") - else: # new game - log_no_spam("connected to new game") - await ctx.disconnect() - ctx.reset_server_state() - ctx.seed_name = None - ctx.got_room_info.clear() - ctx.reset_game_state() - memory.reset_game_state() - - ctx.auth = name - ctx.known_name = name - async_start(ctx.connect()) - await asyncio.wait(( - asyncio.create_task(ctx.got_room_info.wait()), - asyncio.create_task(ctx.exit_event.wait()), - asyncio.create_task(asyncio.sleep(6)) - ), return_when=asyncio.FIRST_COMPLETED) - else: # no name found in game - if not help_message_shown: - logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.') - help_message_shown = True - log_no_spam("looking for connection to game...") - await asyncio.sleep(0.3) - - await asyncio.sleep(0.09375) - logger.info("zillion sync task ending") - - -async def main() -> None: - parser = get_base_parser() - parser.add_argument('diff_file', default="", type=str, nargs="?", - help='Path to a .apzl Archipelago Binary Patch file') - # SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) - args = parser.parse_args() - print(args) - - if args.diff_file: - import Patch - logger.info("patch file was supplied - creating sms rom...") - meta, rom_file = Patch.create_rom_file(args.diff_file) - if "server" in meta: - args.connect = meta["server"] - logger.info(f"wrote rom file to {rom_file}") - - ctx = ZillionContext(args.connect, args.password) - 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() - - sync_task = asyncio.create_task(zillion_sync_task(ctx)) - - await ctx.exit_event.wait() - - ctx.server_address = None - logger.debug("waiting for sync task to end") - await sync_task - logger.debug("sync task ended") - await ctx.shutdown() +import Utils # noqa: E402 +from worlds.zillion.client import launch # noqa: E402 if __name__ == "__main__": Utils.init_logging("ZillionClient", exception_logger="Client") - - colorama.init() - asyncio.run(main()) - colorama.deinit() + launch() diff --git a/worlds/zillion/client.py b/worlds/zillion/client.py new file mode 100644 index 000000000000..ac73f6db50c8 --- /dev/null +++ b/worlds/zillion/client.py @@ -0,0 +1,501 @@ +import asyncio +import base64 +import platform +from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast + +from CommonClient import CommonContext, server_loop, gui_enabled, \ + ClientCommandProcessor, logger, get_base_parser +from NetUtils import ClientStatus +from Utils import async_start + +import colorama + +from zilliandomizer.zri.memory import Memory +from zilliandomizer.zri import events +from zilliandomizer.utils.loc_name_maps import id_to_loc +from zilliandomizer.options import Chars +from zilliandomizer.patch import RescueInfo + +from .id_maps import make_id_to_others +from .config import base_id, zillion_map + + +class ZillionCommandProcessor(ClientCommandProcessor): + ctx: "ZillionContext" + + def _cmd_sms(self) -> None: + """ Tell the client that Zillion is running in RetroArch. """ + logger.info("ready to look for game") + self.ctx.look_for_retroarch.set() + + def _cmd_map(self) -> None: + """ Toggle view of the map tracker. """ + self.ctx.ui_toggle_map() + + +class ToggleCallback(Protocol): + def __call__(self) -> None: ... + + +class SetRoomCallback(Protocol): + def __call__(self, rooms: List[List[int]]) -> None: ... + + +class ZillionContext(CommonContext): + game = "Zillion" + command_processor = ZillionCommandProcessor + items_handling = 1 # receive items from other players + + known_name: Optional[str] + """ This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """ + + from_game: "asyncio.Queue[events.EventFromGame]" + to_game: "asyncio.Queue[events.EventToGame]" + ap_local_count: int + """ local checks watched by server """ + next_item: int + """ index in `items_received` """ + ap_id_to_name: Dict[int, str] + ap_id_to_zz_id: Dict[int, int] + start_char: Chars = "JJ" + rescues: Dict[int, RescueInfo] = {} + loc_mem_to_id: Dict[int, int] = {} + got_room_info: asyncio.Event + """ flag for connected to server """ + got_slot_data: asyncio.Event + """ serves as a flag for whether I am logged in to the server """ + + look_for_retroarch: asyncio.Event + """ + There is a bug in Python in Windows + https://github.com/python/cpython/issues/91227 + that makes it so if I look for RetroArch before it's ready, + it breaks the asyncio udp transport system. + + As a workaround, we don't look for RetroArch until this event is set. + """ + + ui_toggle_map: ToggleCallback + ui_set_rooms: SetRoomCallback + """ parameter is y 16 x 8 numbers to show in each room """ + + def __init__(self, + server_address: str, + password: str) -> None: + super().__init__(server_address, password) + self.known_name = None + self.from_game = asyncio.Queue() + self.to_game = asyncio.Queue() + self.got_room_info = asyncio.Event() + self.got_slot_data = asyncio.Event() + self.ui_toggle_map = lambda: None + self.ui_set_rooms = lambda rooms: None + + self.look_for_retroarch = asyncio.Event() + if platform.system() != "Windows": + # asyncio udp bug is only on Windows + self.look_for_retroarch.set() + + self.reset_game_state() + + def reset_game_state(self) -> None: + for _ in range(self.from_game.qsize()): + self.from_game.get_nowait() + for _ in range(self.to_game.qsize()): + self.to_game.get_nowait() + self.got_slot_data.clear() + + self.ap_local_count = 0 + self.next_item = 0 + self.ap_id_to_name = {} + self.ap_id_to_zz_id = {} + self.rescues = {} + self.loc_mem_to_id = {} + + self.locations_checked.clear() + self.missing_locations.clear() + self.checked_locations.clear() + self.finished_game = False + self.items_received.clear() + + # override + def on_deathlink(self, data: Dict[str, Any]) -> None: + self.to_game.put_nowait(events.DeathEventToGame()) + return super().on_deathlink(data) + + # override + async def server_auth(self, password_requested: bool = False) -> None: + if password_requested and not self.password: + await super().server_auth(password_requested) + if not self.auth: + logger.info('waiting for connection to game...') + return + logger.info("logging in to server...") + await self.send_connect() + + # override + def run_gui(self) -> None: + from kvui import GameManager + from kivy.core.text import Label as CoreLabel + from kivy.graphics import Ellipse, Color, Rectangle + from kivy.uix.layout import Layout + from kivy.uix.widget import Widget + + class ZillionManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago Zillion Client" + + class MapPanel(Widget): + MAP_WIDTH: ClassVar[int] = 281 + + _number_textures: List[Any] = [] + rooms: List[List[int]] = [] + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + self.rooms = [[0 for _ in range(8)] for _ in range(16)] + + self._make_numbers() + self.update_map() + + self.bind(pos=self.update_map) + # self.bind(size=self.update_bg) + + def _make_numbers(self) -> None: + self._number_textures = [] + for n in range(10): + label = CoreLabel(text=str(n), font_size=22, color=(0.1, 0.9, 0, 1)) + label.refresh() + self._number_textures.append(label.texture) + + def update_map(self, *args: Any) -> None: + self.canvas.clear() + + with self.canvas: + Color(1, 1, 1, 1) + Rectangle(source=zillion_map, + pos=self.pos, + size=(ZillionManager.MapPanel.MAP_WIDTH, + int(ZillionManager.MapPanel.MAP_WIDTH * 1.456))) # aspect ratio of that image + for y in range(16): + for x in range(8): + num = self.rooms[15 - y][x] + if num > 0: + Color(0, 0, 0, 0.4) + pos = [self.pos[0] + 17 + x * 32, self.pos[1] + 14 + y * 24] + Ellipse(size=[22, 22], pos=pos) + Color(1, 1, 1, 1) + pos = [self.pos[0] + 22 + x * 32, self.pos[1] + 12 + y * 24] + num_texture = self._number_textures[num] + Rectangle(texture=num_texture, size=num_texture.size, pos=pos) + + def build(self) -> Layout: + container = super().build() + self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=0) + self.main_area_container.add_widget(self.map_widget) + return container + + def toggle_map_width(self) -> None: + if self.map_widget.width == 0: + self.map_widget.width = ZillionManager.MapPanel.MAP_WIDTH + else: + self.map_widget.width = 0 + self.container.do_layout() + + def set_rooms(self, rooms: List[List[int]]) -> None: + self.map_widget.rooms = rooms + self.map_widget.update_map() + + self.ui = ZillionManager(self) + self.ui_toggle_map = lambda: self.ui.toggle_map_width() + self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms) + run_co: Coroutine[Any, Any, None] = self.ui.async_run() + self.ui_task = asyncio.create_task(run_co, name="UI") + + def on_package(self, cmd: str, args: Dict[str, Any]) -> None: + self.room_item_numbers_to_ui() + if cmd == "Connected": + logger.info("logged in to Archipelago server") + if "slot_data" not in args: + logger.warn("`Connected` packet missing `slot_data`") + return + slot_data = args["slot_data"] + + if "start_char" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `start_char`") + return + self.start_char = slot_data['start_char'] + if self.start_char not in {"Apple", "Champ", "JJ"}: + logger.warn("invalid Zillion `Connected` packet, " + f"`slot_data` `start_char` has invalid value: {self.start_char}") + + if "rescues" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `rescues`") + return + rescues = slot_data["rescues"] + self.rescues = {} + for rescue_id, json_info in rescues.items(): + assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}" + # TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch? + assert json_info["start_char"] == self.start_char, \ + f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}' + ri = RescueInfo(json_info["start_char"], + json_info["room_code"], + json_info["mask"]) + self.rescues[0 if rescue_id == "0" else 1] = ri + + if "loc_mem_to_id" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`") + return + loc_mem_to_id = slot_data["loc_mem_to_id"] + self.loc_mem_to_id = {} + for mem_str, id_str in loc_mem_to_id.items(): + mem = int(mem_str) + id_ = int(id_str) + room_i = mem // 256 + assert 0 <= room_i < 74 + assert id_ in id_to_loc + self.loc_mem_to_id[mem] = id_ + + if len(self.loc_mem_to_id) != 394: + logger.warn("invalid Zillion `Connected` packet, " + f"`slot_data` missing locations in `loc_mem_to_id` - len {len(self.loc_mem_to_id)}") + + self.got_slot_data.set() + + payload = { + "cmd": "Get", + "keys": [f"zillion-{self.auth}-doors"] + } + async_start(self.send_msgs([payload])) + elif cmd == "Retrieved": + if "keys" not in args: + logger.warning(f"invalid Retrieved packet to ZillionClient: {args}") + return + keys = cast(Dict[str, Optional[str]], args["keys"]) + doors_b64 = keys.get(f"zillion-{self.auth}-doors", None) + if doors_b64: + logger.info("received door data from server") + doors = base64.b64decode(doors_b64) + self.to_game.put_nowait(events.DoorEventToGame(doors)) + elif cmd == "RoomInfo": + self.seed_name = args["seed_name"] + self.got_room_info.set() + + def room_item_numbers_to_ui(self) -> None: + rooms = [[0 for _ in range(8)] for _ in range(16)] + for loc_id in self.missing_locations: + loc_id_small = loc_id - base_id + loc_name = id_to_loc[loc_id_small] + y = ord(loc_name[0]) - 65 + x = ord(loc_name[2]) - 49 + if y == 9 and x == 5: + # don't show main computer in numbers + continue + assert (0 <= y < 16) and (0 <= x < 8), f"invalid index from location name {loc_name}" + rooms[y][x] += 1 + # TODO: also add locations with locals lost from loading save state or reset + self.ui_set_rooms(rooms) + + def process_from_game_queue(self) -> None: + if self.from_game.qsize(): + event_from_game = self.from_game.get_nowait() + if isinstance(event_from_game, events.AcquireLocationEventFromGame): + server_id = event_from_game.id + base_id + loc_name = id_to_loc[event_from_game.id] + self.locations_checked.add(server_id) + if server_id in self.missing_locations: + self.ap_local_count += 1 + n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win + logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})') + async_start(self.send_msgs([ + {"cmd": 'LocationChecks', "locations": [server_id]} + ])) + else: + # This will happen a lot in Zillion, + # because all the key words are local and unwatched by the server. + logger.debug(f"DEBUG: {loc_name} not in missing") + elif isinstance(event_from_game, events.DeathEventFromGame): + async_start(self.send_death()) + elif isinstance(event_from_game, events.WinEventFromGame): + if not self.finished_game: + async_start(self.send_msgs([ + {"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL} + ])) + self.finished_game = True + elif isinstance(event_from_game, events.DoorEventFromGame): + if self.auth: + doors_b64 = base64.b64encode(event_from_game.doors).decode() + payload = { + "cmd": "Set", + "key": f"zillion-{self.auth}-doors", + "operations": [{"operation": "replace", "value": doors_b64}] + } + async_start(self.send_msgs([payload])) + else: + logger.warning(f"WARNING: unhandled event from game {event_from_game}") + + def process_items_received(self) -> None: + if len(self.items_received) > self.next_item: + zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received] + for index in range(self.next_item, len(self.items_received)): + ap_id = self.items_received[index].item + from_name = self.player_names[self.items_received[index].player] + # TODO: colors in this text, like sni client? + logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}') + self.to_game.put_nowait( + events.ItemEventToGame(zz_item_ids) + ) + self.next_item = len(self.items_received) + + +def name_seed_from_ram(data: bytes) -> Tuple[str, str]: + """ returns player name, and end of seed string """ + if len(data) == 0: + # no connection to game + return "", "xxx" + null_index = data.find(b'\x00') + if null_index == -1: + logger.warning(f"invalid game id in rom {repr(data)}") + null_index = len(data) + name = data[:null_index].decode() + null_index_2 = data.find(b'\x00', null_index + 1) + if null_index_2 == -1: + null_index_2 = len(data) + seed_name = data[null_index + 1:null_index_2].decode() + + return name, seed_name + + +async def zillion_sync_task(ctx: ZillionContext) -> None: + logger.info("started zillion sync task") + + # to work around the Python bug where we can't check for RetroArch + if not ctx.look_for_retroarch.is_set(): + logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.") + await asyncio.wait(( + asyncio.create_task(ctx.look_for_retroarch.wait()), + asyncio.create_task(ctx.exit_event.wait()) + ), return_when=asyncio.FIRST_COMPLETED) + + last_log = "" + + def log_no_spam(msg: str) -> None: + nonlocal last_log + if msg != last_log: + last_log = msg + logger.info(msg) + + # to only show this message once per client run + help_message_shown = False + + with Memory(ctx.from_game, ctx.to_game) as memory: + while not ctx.exit_event.is_set(): + ram = await memory.read() + game_id = memory.get_rom_to_ram_data(ram) + name, seed_end = name_seed_from_ram(game_id) + if len(name): + if name == ctx.known_name: + ctx.auth = name + # this is the name we know + if ctx.server and ctx.server.socket: # type: ignore + if ctx.got_room_info.is_set(): + if ctx.seed_name and ctx.seed_name.endswith(seed_end): + # correct seed + if memory.have_generation_info(): + log_no_spam("everything connected") + await memory.process_ram(ram) + ctx.process_from_game_queue() + ctx.process_items_received() + else: # no generation info + if ctx.got_slot_data.is_set(): + memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id) + ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \ + make_id_to_others(ctx.start_char) + ctx.next_item = 0 + ctx.ap_local_count = len(ctx.checked_locations) + else: # no slot data yet + async_start(ctx.send_connect()) + log_no_spam("logging in to server...") + await asyncio.wait(( + asyncio.create_task(ctx.got_slot_data.wait()), + asyncio.create_task(ctx.exit_event.wait()), + asyncio.create_task(asyncio.sleep(6)) + ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets + else: # not correct seed name + log_no_spam("incorrect seed - did you mix up roms?") + else: # no room info + # If we get here, it looks like `RoomInfo` packet got lost + log_no_spam("waiting for room info from server...") + else: # server not connected + log_no_spam("waiting for server connection...") + else: # new game + log_no_spam("connected to new game") + await ctx.disconnect() + ctx.reset_server_state() + ctx.seed_name = None + ctx.got_room_info.clear() + ctx.reset_game_state() + memory.reset_game_state() + + ctx.auth = name + ctx.known_name = name + async_start(ctx.connect()) + await asyncio.wait(( + asyncio.create_task(ctx.got_room_info.wait()), + asyncio.create_task(ctx.exit_event.wait()), + asyncio.create_task(asyncio.sleep(6)) + ), return_when=asyncio.FIRST_COMPLETED) + else: # no name found in game + if not help_message_shown: + logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.') + help_message_shown = True + log_no_spam("looking for connection to game...") + await asyncio.sleep(0.3) + + await asyncio.sleep(0.09375) + logger.info("zillion sync task ending") + + +async def main() -> None: + parser = get_base_parser() + parser.add_argument('diff_file', default="", type=str, nargs="?", + help='Path to a .apzl Archipelago Binary Patch file') + # SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) + args = parser.parse_args() + print(args) + + if args.diff_file: + import Patch + logger.info("patch file was supplied - creating sms rom...") + meta, rom_file = Patch.create_rom_file(args.diff_file) + if "server" in meta: + args.connect = meta["server"] + logger.info(f"wrote rom file to {rom_file}") + + ctx = ZillionContext(args.connect, args.password) + 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() + + sync_task = asyncio.create_task(zillion_sync_task(ctx)) + + await ctx.exit_event.wait() + + ctx.server_address = None + logger.debug("waiting for sync task to end") + await sync_task + logger.debug("sync task ended") + await ctx.shutdown() + + +def launch() -> None: + colorama.init() + asyncio.run(main()) + colorama.deinit() From 88c7484b3a105cea8adbb8ade5c2afd80fff4c4c Mon Sep 17 00:00:00 2001 From: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Tue, 2 Jan 2024 03:16:45 -0700 Subject: [PATCH 068/282] Shivers: Fixes rule logic for location 'puzzle solved three floor elevator' (#2657) Fixes rule logic for location 'puzzle solved three floor elevator'. Missing a parenthesis caused only the key requirement to be checked for the blue maze region. --- worlds/shivers/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py index fdd260ca91aa..4e1058fecfc8 100644 --- a/worlds/shivers/Rules.py +++ b/worlds/shivers/Rules.py @@ -157,7 +157,7 @@ def get_rules_lookup(player: int): "Puzzle Solved Underground Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player) and state.has("Key for Office Elevator", player))), "Puzzle Solved Bedroom Elevator": lambda state: (state.can_reach("Office", "Region", player) and state.has_all({"Key for Bedroom Elevator","Crawling"}, player)), - "Puzzle Solved Three Floor Elevator": lambda state: ((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player) + "Puzzle Solved Three Floor Elevator": lambda state: (((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player)) and state.has("Key for Three Floor Elevator", player))) }, "lightning": { From e5c739ee31c43450dd5845768fa459f98e917dce Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Tue, 2 Jan 2024 05:19:57 -0500 Subject: [PATCH 069/282] KH2: Ability dupe fix and stat increase fix (#2621) Makes the client make sure the player has the correct amount of stat increase instead of letting the goa mod (apcompanion) do it abilities: checks the slot where abilities could dupe unless that slot is being used for an actual abiliity given to the player --- worlds/kh2/Client.py | 95 +++++++++++++++++++++++++++++++++++--------- worlds/kh2/Rules.py | 5 +-- 2 files changed, 79 insertions(+), 21 deletions(-) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index a5be06c7fb16..544e710741b4 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -80,11 +80,6 @@ def __init__(self, server_address, password): }, }, } - self.front_of_inventory = { - "Sora": 0x2546, - "Donald": 0x2658, - "Goofy": 0x276C, - } self.kh2seedname = None self.kh2slotdata = None self.itemamount = {} @@ -169,6 +164,14 @@ def __init__(self, server_address, password): self.ability_code_list = None self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"} + self.base_hp = 20 + self.base_mp = 100 + self.base_drive = 5 + self.base_accessory_slots = 1 + self.base_armor_slots = 1 + self.base_item_slots = 3 + self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E, 0x2770, 0x2772] + async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: await super(KH2Context, self).server_auth(password_requested) @@ -219,6 +222,12 @@ def kh2_write_byte(self, address, value): def kh2_read_byte(self, address): return int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + address, 1), "big") + def kh2_read_int(self, address): + return self.kh2.read_int(self.kh2.base_address + address) + + def kh2_write_int(self, address, value): + self.kh2.write_int(self.kh2.base_address + address, value) + def on_package(self, cmd: str, args: dict): if cmd in {"RoomInfo"}: self.kh2seedname = args['seed_name'] @@ -476,7 +485,7 @@ async def verifyLevel(self): async def give_item(self, item, location): try: - # todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites + # todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites itemname = self.lookup_id_to_item[item] itemdata = self.item_name_to_data[itemname] # itemcode = self.kh2_item_name_to_id[itemname] @@ -507,6 +516,8 @@ async def give_item(self, item, location): ability_slot = self.kh2_seed_save_cache["GoofyInvo"][1] self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) self.kh2_seed_save_cache["GoofyInvo"][1] -= 2 + if ability_slot in self.front_ability_slots: + self.front_ability_slots.remove(ability_slot) elif len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \ self.AbilityQuantityDict[itemname]: @@ -518,11 +529,14 @@ async def give_item(self, item, location): ability_slot = self.kh2_seed_save_cache["DonaldInvo"][0] self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) self.kh2_seed_save_cache["DonaldInvo"][0] -= 2 - elif itemname in self.goofy_ability_set: + else: ability_slot = self.kh2_seed_save_cache["GoofyInvo"][0] self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) self.kh2_seed_save_cache["GoofyInvo"][0] -= 2 + if ability_slot in self.front_ability_slots: + self.front_ability_slots.remove(ability_slot) + elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}: # if memaddr is in a bitmask location in memory if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]: @@ -615,7 +629,7 @@ async def verifyItems(self): master_sell = master_equipment | master_staff | master_shield await asyncio.create_task(self.IsInShop(master_sell)) - + # print(self.kh2_seed_save_cache["AmountInvo"]["Ability"]) for item_name in master_amount: item_data = self.item_name_to_data[item_name] amount_of_items = 0 @@ -673,10 +687,10 @@ async def verifyItems(self): self.kh2_write_short(self.Save + slot, item_data.memaddr) # removes the duped ability if client gave faster than the game. - for charInvo in {"Sora", "Donald", "Goofy"}: - if self.kh2_read_short(self.Save + self.front_of_inventory[charInvo]) != 0: - print(f"removed {self.Save + self.front_of_inventory[charInvo]} from {charInvo}") - self.kh2_write_short(self.Save + self.front_of_inventory[charInvo], 0) + for ability in self.front_ability_slots: + if self.kh2_read_short(self.Save + ability) != 0: + print(f"removed {self.Save + ability} from {ability}") + self.kh2_write_short(self.Save + ability, 0) # remove the dummy level 1 growths if they are in these invo slots. for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}: @@ -740,15 +754,60 @@ async def verifyItems(self): self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) for item_name in master_stat: - item_data = self.item_name_to_data[item_name] amount_of_items = 0 amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][item_name] + if self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5: + if item_name == ItemName.MaxHPUp: + if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical + Bonus = 5 + else: # Critical + Bonus = 2 + if self.kh2_read_int(self.Slot1 + 0x004) != self.base_hp + (Bonus * amount_of_items): + self.kh2_write_int(self.Slot1 + 0x004, self.base_hp + (Bonus * amount_of_items)) + + elif item_name == ItemName.MaxMPUp: + if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical + Bonus = 10 + else: # Critical + Bonus = 5 + if self.kh2_read_int(self.Slot1 + 0x184) != self.base_mp + (Bonus * amount_of_items): + self.kh2_write_int(self.Slot1 + 0x184, self.base_mp + (Bonus * amount_of_items)) + + elif item_name == ItemName.DriveGaugeUp: + current_max_drive = self.kh2_read_byte(self.Slot1 + 0x1B2) + # change when max drive is changed from 6 to 4 + if current_max_drive < 9 and current_max_drive != self.base_drive + amount_of_items: + self.kh2_write_byte(self.Slot1 + 0x1B2, self.base_drive + amount_of_items) + + elif item_name == ItemName.AccessorySlotUp: + current_accessory = self.kh2_read_byte(self.Save + 0x2501) + if current_accessory != self.base_accessory_slots + amount_of_items: + if 4 > current_accessory < self.base_accessory_slots + amount_of_items: + self.kh2_write_byte(self.Save + 0x2501, current_accessory + 1) + elif self.base_accessory_slots + amount_of_items < 4: + self.kh2_write_byte(self.Save + 0x2501, self.base_accessory_slots + amount_of_items) + + elif item_name == ItemName.ArmorSlotUp: + current_armor_slots = self.kh2_read_byte(self.Save + 0x2500) + if current_armor_slots != self.base_armor_slots + amount_of_items: + if 4 > current_armor_slots < self.base_armor_slots + amount_of_items: + self.kh2_write_byte(self.Save + 0x2500, current_armor_slots + 1) + elif self.base_armor_slots + amount_of_items < 4: + self.kh2_write_byte(self.Save + 0x2500, self.base_armor_slots + amount_of_items) + + elif item_name == ItemName.ItemSlotUp: + current_item_slots = self.kh2_read_byte(self.Save + 0x2502) + if current_item_slots != self.base_item_slots + amount_of_items: + if 8 > current_item_slots < self.base_item_slots + amount_of_items: + self.kh2_write_byte(self.Save + 0x2502, current_item_slots + 1) + elif self.base_item_slots + amount_of_items < 8: + self.kh2_write_byte(self.Save + 0x2502, self.base_item_slots + amount_of_items) + + # if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \ + # and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \ + # self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}: + # self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) - # if slot1 has 5 drive gauge and goa lost illusion is checked and they are not in a cutscene - if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \ - and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \ - self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}: - self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) if "PoptrackerVersionCheck" in self.kh2slotdata: if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(self.Save + 0x3607) != 1: # telling the goa they are on version 4.3 self.kh2_write_byte(self.Save + 0x3607, 1) diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index 41207c6cb3d0..7c5551dbd563 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -268,7 +268,6 @@ def set_kh2_rules(self) -> None: add_item_rule(location, lambda item: item.player == self.player and item.name in DonaldAbility_Table.keys()) def set_kh2_goal(self): - final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnas, self.player) if self.multiworld.Goal[self.player] == "three_proofs": final_xemnas_location.access_rule = lambda state: self.kh2_has_all(three_proofs, state) @@ -291,8 +290,8 @@ def set_kh2_goal(self): else: self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) else: - final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) and\ - state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) + final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) and \ + state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) if self.multiworld.FinalXemnas[self.player]: self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1) else: From bf17582c5534d3dc35bdb597bcb2da097228e275 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 2 Jan 2024 03:32:03 -0700 Subject: [PATCH 070/282] BizHawkClient: Add some handling for non-string errors (#2656) --- data/lua/connector_bizhawk_generic.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/data/lua/connector_bizhawk_generic.lua b/data/lua/connector_bizhawk_generic.lua index eff400cb032b..47af6e003d8e 100644 --- a/data/lua/connector_bizhawk_generic.lua +++ b/data/lua/connector_bizhawk_generic.lua @@ -456,6 +456,7 @@ function send_receive () failed_guard_response = response end else + if type(response) ~= "string" then response = "Unknown error" end res[i] = {type = "ERROR", err = response} end end From 0df0955415cd4523531ab091f05090831fb5016d Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 2 Jan 2024 08:03:39 -0600 Subject: [PATCH 071/282] Core: check if a location is an event before excluding it (#2653) * Core: check if a location is an event before excluding it * log a warning * put the warning in the right spot --- worlds/generic/Rules.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index 520ad2252568..ac5e1aa50750 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -1,4 +1,5 @@ import collections +import logging import typing from BaseClasses import LocationProgressType, MultiWorld, Location, Region, Entrance @@ -81,15 +82,18 @@ def forbid(sender: int, receiver: int, items: typing.Set[str]): i.name not in sending_blockers[i.player] and old_rule(i) -def exclusion_rules(world: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None: +def exclusion_rules(multiworld: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None: for loc_name in exclude_locations: try: - location = world.get_location(loc_name, player) + location = multiworld.get_location(loc_name, player) except KeyError as e: # failed to find the given location. Check if it's a legitimate location - if loc_name not in world.worlds[player].location_name_to_id: + if loc_name not in multiworld.worlds[player].location_name_to_id: raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e else: - location.progress_type = LocationProgressType.EXCLUDED + if not location.event: + location.progress_type = LocationProgressType.EXCLUDED + else: + logging.warning(f"Unable to exclude location {loc_name} in player {player}'s world.") def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule): From 7406a1e512b51748cc173a18a52ec747135f054a Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Wed, 3 Jan 2024 18:43:41 -0600 Subject: [PATCH 072/282] WebHost: Copyright update time. (#2660) --- WebHostLib/templates/islandFooter.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/templates/islandFooter.html b/WebHostLib/templates/islandFooter.html index 7b89c4a9e079..08cf227990b8 100644 --- a/WebHostLib/templates/islandFooter.html +++ b/WebHostLib/templates/islandFooter.html @@ -1,6 +1,6 @@ {% block footer %}