From a78863fde1ae6517d99917d54f8935966d151044 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Fri, 26 Aug 2022 02:12:37 -0500 Subject: [PATCH 01/24] Docs: Update community supported libraries in api doc (#788) * Docs: Update client supported libraries in api doc * left align table column * Update table of languages to include Haxe lib and remarks * Reformat table * Changed verbiage on SNI remark --- docs/network protocol.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/network protocol.md b/docs/network protocol.md index 342514248d..3315ddec2d 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -13,9 +13,18 @@ These steps should be followed in order to establish a gameplay connection with In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet. -There are libraries available that implement this network protocol in [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py), [Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java), [.Net](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Net) and [C++](https://github.com/black-sliver/apclientpp) +There are also a number of community-supported libraries available that implement this network protocol to make integrating with Archipelago easier. -For Super Nintendo games there are clients available in either [Node](https://github.com/ArchipelagoMW/SuperNintendoClient) or [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py), There are also game specific clients available for [The Legend of Zelda: Ocarina of Time](https://github.com/ArchipelagoMW/Z5Client) or [Final Fantasy 1](https://github.com/ArchipelagoMW/Archipelago/blob/main/FF1Client.py) +| Language/Runtime | Project | Remarks | +|-------------------------------|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------| +| Python | [Archipelago CommonClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py) | | +| | [Archipelago SNIClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py) | For Super Nintendo Game Support; Utilizes [SNI](https://github.com/alttpo/sni). | +| JVM (Java / Kotlin) | [Archipelago.MultiClient.Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java) | | +| .NET (C# / C++ / F# / VB.NET) | [Archipelago.MultiClient.Net](https://www.nuget.org/packages/Archipelago.MultiClient.Net) | | +| C++ | [apclientpp](https://github.com/black-sliver/apclientpp) | almost-header-only | +| | [APCpp](https://github.com/N00byKing/APCpp) | CMake | +| JavaScript / TypeScript | [archipelago.js](https://www.npmjs.com/package/archipelago.js) | Browser and Node.js Supported | +| Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | | ## Synchronizing Items When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet. From a175aa93e7185d929a471ac0ec4173e78be564cf Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 26 Aug 2022 01:31:30 -0700 Subject: [PATCH 02/24] Factorio: Detect if more than one AP factorio mod is loaded. (#964) --- worlds/factorio/data/mod_template/settings.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/worlds/factorio/data/mod_template/settings.lua b/worlds/factorio/data/mod_template/settings.lua index 7703ebe2e5..73e131a60e 100644 --- a/worlds/factorio/data/mod_template/settings.lua +++ b/worlds/factorio/data/mod_template/settings.lua @@ -1,3 +1,21 @@ +-- Find out if more than one AP mod is loaded, and if so, error out. +function mod_is_AP(str) + -- lua string.match is way more restrictive than regex. Regex would be "^AP-W?\d{20}-P[1-9]\d*-.+$" + local result = string.match(str, "^AP%-W?%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%-P[1-9]%d-%-.+$") + if result ~= nil then + log("Archipelago Mod: " .. result .. " is loaded.") + end + return result ~= nil +end +local ap_mod_count = 0 +for name, _ in pairs(mods) do + if mod_is_AP(name) then + ap_mod_count = ap_mod_count + 1 + if ap_mod_count > 1 then + error("More than one Archipelago Factorio mod is loaded.") + end + end +end data:extend({ { type = "bool-setting", From af19180ff0834bf8af09a1e989c44fa794cc3802 Mon Sep 17 00:00:00 2001 From: strotlog <49286967+strotlog@users.noreply.github.com> Date: Sat, 20 Aug 2022 04:35:46 +0000 Subject: [PATCH 03/24] SM: Fix rolling saves, add SRAM features - fix receiving items in an old save (issue #855) by moving receive queue's read pointer to a per-saveslot value - clear SRAM over $70:2000, and invalidate save data, when booting a new seed number for the first time - copy important ROM data to SRAM so future clients don't have to read ROM --- .../multiworld-basepatch.ips | Bin 17952 -> 18193 bytes .../data/SMBasepatch_prebuilt/multiworld.sym | 801 ++++++++++-------- .../sm-basepatch-symbols.json | 74 +- 3 files changed, 500 insertions(+), 375 deletions(-) diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips index d7fd17613e8f2af2b5cc6ed5db11efbe9d227f07..7ac3ea018475a0db495d6d540c9dda5f66f1391f 100644 GIT binary patch delta 499 zcmX|-PiPZC6vkh6(=?ca4VZ&}Vuq0{5d;TMo&_<~KTz}}9z$n`_Dqx~_b0GY=;FgE&o_ewRsM z6u;h2Vsnx4y>Sqp#q`_3L&n3jr=WX;ZkC?5{9Ao$>QZtAuoxjoY+a>2Bro&gDL-B#&u+)JeSKm9bBrWrO3JH=ddlGKyiAi&2&ro{n^*xHbPqe+AI% zIpOfSk=&Z!ob3|l1H4q*#Bo?Ma*i`MEEv&Y1pm8oUzAYe_%5R+P@kYCQCAsvPvkG1 zFX%gU0HYY)PSNF#6%wzXno~!8G&-ptSdX~S-JC&0^W-2w*(d*X53D_+n qEe}V|L_gfCnwGwXb8zSnQoG8bze6`HcSXCL7+MA`Tg|sxIrj%vd*R;z delta 239 zcmbQ($GD(}ae|S2LBkG)hVKk4G4~sHFl=UKU{jJgvE$V%M()iFEJ`ODHwLOPG1WJ2 zu4Hmo@?!jO$Y4Fg|8pN$lmr`gJTh3>pY3rgiUTP4Pp#nOzf&JnRx&UwVqlohpinTM zK?R64fLN#C#ft|FEEis`{CBD!s8FNoSH@&!H66M23~B}aEI@&bRSOsxvOPcs0lED^ zVU3LJ$-Zg@%s}Sk18ROu?;9oysEcuLXaMPv0;!m6sIDgH*|1|j!-E|kxc4(X*s)4! fQp4m7b$!174cY61IO`-b1sIf?8#d2XFOdfTlAm2m diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym b/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym index 751f470f53..5def2b7d9c 100644 --- a/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym +++ b/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym @@ -2,14 +2,14 @@ ; generated by asar [labels] -B8:8026 :neg_1_1 +B8:80C1 :neg_1_1 85:B9B4 :neg_1_2 85:B9E6 :neg_1_3 B8:C81F :neg_1_4 B8:C831 :neg_1_5 B8:C843 :neg_1_6 B8:800C :pos_1_0 -B8:81DE :pos_1_1 +B8:82D7 :pos_1_1 84:FA6B :pos_1_2 84:FA75 :pos_1_3 B8:C862 :pos_1_4 @@ -20,7 +20,7 @@ B8:C87C :pos_1_6 85:990F CLIPLEN_end 85:990C CLIPLEN_no_multi 85:FF1D CLIPSET -B8:80EF COLLECTTANK +B8:81E8 COLLECTTANK 85:FF45 MISCFX 84:8BF2 NORMAL 85:FF4E SETFX @@ -38,6 +38,11 @@ CE:FF00 config_multiworld CE:FF08 config_player_id CE:FF06 config_remote_items CE:FF02 config_sprite +B8:8119 copy_config_to_sram +B8:80FD copy_memory +B8:8117 copy_memory_done +B8:8109 copy_memory_even +B8:810F copy_memory_loop 84:F894 h_item 84:F8AD i_chozo_item 84:F8B4 i_hidden_item @@ -46,11 +51,11 @@ B8:885C i_item_setup_shared B8:8878 i_item_setup_shared_all_items B8:8883 i_item_setup_shared_alwaysloaded 84:FA79 i_live_pickup -B8:817F i_live_pickup_multiworld -B8:81C4 i_live_pickup_multiworld_end -B8:819B i_live_pickup_multiworld_local_item_or_offworld -B8:81B0 i_live_pickup_multiworld_own_item -B8:81BC i_live_pickup_multiworld_own_item1 +B8:8278 i_live_pickup_multiworld +B8:82BD i_live_pickup_multiworld_end +B8:8294 i_live_pickup_multiworld_local_item_or_offworld +B8:82A9 i_live_pickup_multiworld_own_item +B8:82B5 i_live_pickup_multiworld_own_item1 84:FA1E i_load_custom_graphics 84:FA39 i_load_custom_graphics_all_items 84:FA49 i_load_custom_graphics_alwaysloaded @@ -85,22 +90,27 @@ B8:81BC i_live_pickup_multiworld_own_item1 85:B9CA message_write_placeholders_loop 85:B9DC message_write_placeholders_notfound 85:B9DF message_write_placeholders_value_ok -B8:8092 mw_display_item_sent -B8:80FF mw_handle_queue -B8:8178 mw_handle_queue_end -B8:8101 mw_handle_queue_loop -B8:8151 mw_handle_queue_new_remote_item -B8:816D mw_handle_queue_next -B8:8163 mw_handle_queue_perform_receive -B8:81C8 mw_hook_main_game +B8:818B mw_display_item_sent +B8:81F8 mw_handle_queue +B8:8271 mw_handle_queue_end +B8:81FA mw_handle_queue_loop +B8:824A mw_handle_queue_new_remote_item +B8:8266 mw_handle_queue_next +B8:825C mw_handle_queue_perform_receive +B8:82C1 mw_hook_main_game B8:8011 mw_init -B8:8044 mw_init_end +B8:8066 mw_init_continuereset +B8:80EA mw_init_end B8:8000 mw_init_memory -B8:8083 mw_load_sram -B8:80B0 mw_receive_item -B8:80E8 mw_receive_item_end -B8:8070 mw_save_sram -B8:8049 mw_write_message +B8:803B mw_init_reset_sram +B8:8051 mw_init_smstringdata +B8:8174 mw_load_sram +B8:8182 mw_load_sram_done +B8:8185 mw_load_sram_setnewgame +B8:81A9 mw_receive_item +B8:81E1 mw_receive_item_end +B8:8169 mw_save_sram +B8:8142 mw_write_message 84:F888 nonprog_item_eight_palette_indices 89:9200 offworld_graphics_data_item 89:9100 offworld_graphics_data_progression_item @@ -125,7 +135,7 @@ B8:8049 mw_write_message 84:F96E p_visible_item_end 84:F95B p_visible_item_loop 84:F967 p_visible_item_trigger -B8:81DF patch_load_multiworld +B8:82D8 patch_load_multiworld 84:FA7E perform_item_pickup 84:F886 plm_graphics_entry_offworld_item 84:F87C plm_graphics_entry_offworld_progression_item @@ -144,17 +154,19 @@ B8:C808 start_item_data_minor B8:C818 start_item_data_reserve B8:C856 update_graphic 84:F890 v_item +B8:80EF write_repeated_memory +B8:80F4 write_repeated_memory_loop [source files] 0000 e25029c5 main.asm 0001 06780555 ../common/nofanfare.asm -0002 e76d1f83 ../common/multiworld.asm +0002 4f9a780e ../common/multiworld.asm 0003 613d24e1 ../common/itemextras.asm 0004 d6616c0c ../common/items.asm 0005 440b54fe ../common/startitem.asm [rom checksum] -09b134c5 +ad81eda1 [addr-to-line mapping] ff:ffff 0000:00000001 @@ -204,330 +216,423 @@ ff:ffff 0000:00000001 84:8bf2 0001:00000152 84:8bf6 0001:00000153 84:8bf7 0001:00000153 -b8:8000 0002:00000019 -b8:8002 0002:0000001a -b8:8006 0002:0000001b -b8:8008 0002:0000001c -b8:800c 0002:00000020 -b8:800e 0002:00000021 -b8:8010 0002:00000022 -b8:8011 0002:00000025 -b8:8012 0002:00000025 -b8:8013 0002:00000025 -b8:8014 0002:00000025 +b8:8000 0002:0000005a +b8:8002 0002:0000005b +b8:8006 0002:0000005c +b8:8008 0002:0000005d +b8:800c 0002:00000061 +b8:800e 0002:00000062 +b8:8010 0002:00000063 +b8:8011 0002:00000066 +b8:8012 0002:00000066 +b8:8013 0002:00000066 +b8:8014 0002:00000066 b8:8015 0000:00000013 -b8:8017 0002:00000029 -b8:801b 0002:0000002a -b8:801e 0002:0000002b -b8:8020 0002:0000002d -b8:8023 0002:0000002e -b8:8026 0002:00000031 -b8:802a 0002:00000032 -b8:802e 0002:00000033 -b8:8032 0002:00000034 -b8:8036 0002:00000035 -b8:8037 0002:00000035 -b8:8038 0002:00000036 -b8:803b 0002:00000037 -b8:803d 0002:00000039 -b8:8040 0002:0000003a -b8:8044 0002:0000003d -b8:8045 0002:0000003d -b8:8046 0002:0000003d -b8:8047 0002:0000003d -b8:8048 0002:0000003e -b8:8049 0002:00000043 -b8:804a 0002:00000043 -b8:804b 0002:00000044 -b8:804c 0002:00000044 -b8:804d 0002:00000045 -b8:8051 0002:00000046 -b8:8054 0002:00000046 -b8:8055 0002:00000047 -b8:8056 0002:00000048 -b8:805a 0002:00000049 -b8:805b 0002:0000004a -b8:805f 0002:0000004b -b8:8060 0002:0000004c -b8:8064 0002:0000004e -b8:8068 0002:0000004f -b8:8069 0002:00000050 -b8:806d 0002:00000051 -b8:806e 0002:00000051 -b8:806f 0002:00000052 -b8:8070 0002:00000055 -b8:8071 0002:00000055 -b8:8072 0000:00000013 -b8:8074 0002:00000057 -b8:8078 0002:00000058 -b8:807c 0002:00000059 -b8:807d 0002:00000059 -b8:807e 0002:0000005b -b8:807f 0002:0000005c -b8:8082 0002:0000005d -b8:8083 0002:00000060 -b8:8084 0002:00000060 -b8:8085 0000:00000013 -b8:8087 0002:00000062 -b8:808b 0002:00000063 -b8:808f 0002:00000064 -b8:8090 0002:00000064 -b8:8091 0002:00000065 -b8:8092 0002:0000006a -b8:8094 0002:0000006b -b8:8096 0002:0000006e -b8:8099 0002:0000006f -b8:809b 0002:00000070 -b8:809e 0002:00000071 -b8:80a0 0002:00000072 -b8:80a3 0002:00000073 -b8:80a7 0002:00000074 -b8:80a9 0002:00000075 -b8:80ab 0002:00000076 -b8:80ad 0002:00000077 -b8:80af 0002:00000078 -b8:80b0 0002:0000007c -b8:80b1 0002:0000007c -b8:80b2 0002:0000007d -b8:80b5 0002:0000007e -b8:80b7 0002:0000007f -b8:80ba 0002:00000080 -b8:80bc 0002:00000081 -b8:80bd 0002:00000082 -b8:80be 0002:00000084 -b8:80c1 0002:00000085 -b8:80c3 0002:00000086 -b8:80c6 0002:00000087 -b8:80c7 0002:00000088 -b8:80ca 0002:00000089 -b8:80cb 0002:00000089 -b8:80cc 0002:0000008a -b8:80d0 0002:0000008b -b8:80d1 0002:0000008c -b8:80d4 0002:0000008d -b8:80d8 0002:0000008e -b8:80da 0002:00000090 -b8:80dd 0002:00000091 -b8:80df 0002:00000092 -b8:80e2 0002:00000093 -b8:80e4 0002:00000095 -b8:80e8 0002:00000097 -b8:80ea 0002:00000098 -b8:80ec 0002:00000099 -b8:80ed 0002:00000099 -b8:80ee 0002:0000009a -b8:80ef 0002:000000a5 -b8:80f0 0002:000000a6 -b8:80f4 0002:000000a7 -b8:80f5 0002:000000a8 -b8:80f9 0002:000000a9 -b8:80fa 0002:000000ab -b8:80fe 0002:000000ac -b8:80ff 0002:000000de -b8:8100 0002:000000de -b8:8101 0002:000000e1 -b8:8105 0002:000000e2 -b8:8109 0002:000000e3 -b8:810b 0002:000000e5 -b8:810d 0002:000000e5 -b8:810e 0002:000000e8 -b8:8112 0002:000000e9 -b8:8114 0002:000000ea -b8:8118 0002:000000eb -b8:811a 0002:000000ec -b8:811e 0002:000000ed -b8:8121 0002:000000ee -b8:8123 0002:000000ef -b8:8125 0002:000000f0 -b8:8129 0002:000000f1 -b8:812b 0002:000000f2 -b8:812d 0002:000000f3 -b8:8130 0002:000000f4 -b8:8133 0002:000000f5 -b8:8135 0002:000000f6 -b8:813d 0002:000000fa -b8:813e 0002:000000fb -b8:813f 0002:000000fc -b8:8143 0002:000000ff -b8:8147 0002:00000100 -b8:814b 0002:00000101 -b8:814d 0002:00000103 -b8:814e 0002:00000104 -b8:814f 0002:00000105 -b8:8151 0002:0000010a -b8:8152 0002:0000010b -b8:8156 0002:0000010e -b8:815a 0002:0000010f -b8:815e 0002:00000110 -b8:8162 0002:00000111 -b8:8163 0002:00000115 -b8:8165 0002:00000116 -b8:8168 0002:00000117 -b8:816a 0002:00000118 -b8:816d 0002:0000011b -b8:8171 0002:0000011c -b8:8172 0002:0000011d -b8:8176 0002:0000011f -b8:8178 0002:00000122 -b8:817a 0002:00000123 -b8:817c 0002:00000124 -b8:817d 0002:00000124 -b8:817e 0002:00000125 -b8:817f 0002:00000129 -b8:8180 0002:00000129 -b8:8181 0002:00000129 -b8:8182 0002:0000012a -b8:8186 0002:0000012b -b8:8189 0002:0000012b -b8:818a 0002:0000012d -b8:818e 0002:0000012e -b8:818f 0002:0000012f -b8:8193 0002:00000130 -b8:8196 0002:00000131 -b8:8198 0002:00000133 -b8:819b 0002:00000136 -b8:819f 0002:00000137 -b8:81a3 0002:00000138 -b8:81a5 0002:0000013a -b8:81a9 0002:0000013b -b8:81aa 0002:0000013d -b8:81ae 0002:0000013e -b8:81b0 0002:00000141 -b8:81b4 0002:00000142 -b8:81b7 0002:00000143 -b8:81b9 0002:00000144 -b8:81bc 0002:00000147 -b8:81bd 0002:00000148 -b8:81be 0002:00000149 -b8:81c2 0002:0000014a -b8:81c4 0002:0000014d -b8:81c5 0002:0000014d -b8:81c6 0002:0000014d -b8:81c7 0002:0000014e -b8:81c8 0002:00000152 -b8:81cc 0002:00000153 -b8:81d0 0002:00000154 -b8:81d2 0002:00000155 -b8:81d6 0002:00000156 -b8:81d9 0002:00000157 -b8:81db 0002:00000158 -b8:81de 0002:0000015a -b8:81df 0002:0000015d -b8:81e3 0002:0000015e -b8:81e4 0002:0000015f -b8:81e7 0002:00000160 -b8:81eb 0002:00000162 -b8:81ec 0002:00000163 -b8:81ed 0002:00000164 -b8:81ee 0002:00000165 -b8:81ef 0002:00000166 -8b:914a 0002:0000016b -81:80f7 0002:0000016e -81:8027 0002:00000171 -82:8bb3 0002:00000174 -85:b9a3 0002:0000020e -85:b9a4 0002:0000020e -85:b9a5 0002:00000211 -85:b9a7 0002:00000212 -85:b9ad 0002:00000212 -85:b9ae 0002:00000213 -85:b9b1 0002:00000214 -85:b9b2 0002:00000215 -85:b9b3 0002:00000215 -85:b9b4 0002:00000219 -85:b9b7 0002:0000021a -85:b9bb 0002:0000021b -85:b9bd 0002:0000021b -85:b9bf 0002:0000021c -85:b9c2 0002:0000021d -85:b9c4 0002:0000021f -85:b9c5 0002:00000220 -85:b9c7 0002:00000224 -85:b9ca 0002:00000226 -85:b9cd 0002:00000227 -85:b9cf 0002:00000228 -85:b9d1 0002:00000229 -85:b9d5 0002:0000022a -85:b9d7 0002:0000022b -85:b9d9 0002:0000022c -85:b9da 0002:0000022d -85:b9dc 0002:0000022f -85:b9df 0002:00000231 -85:b9e2 0002:00000231 -85:b9e3 0002:00000232 -85:b9e6 0002:00000234 -85:b9ea 0002:00000235 -85:b9ed 0002:00000236 -85:b9ee 0002:00000237 -85:b9ef 0002:00000237 -85:b9f0 0002:00000238 -85:b9f4 0002:00000239 -85:b9f5 0002:0000023a -85:b9f9 0002:0000023b -85:b9fb 0002:0000023c -85:b9fc 0002:0000023d -85:b9fd 0002:0000023e -85:ba00 0002:0000023f -85:ba02 0002:00000240 -85:ba04 0002:00000243 -85:ba05 0002:00000243 -85:ba06 0002:00000244 -85:ba09 0002:00000245 -85:ba8a 0002:00000253 -85:ba8c 0002:00000254 -85:ba8f 0002:00000255 -85:ba92 0002:00000256 -85:ba95 0002:0000025e -85:ba96 0002:0000025f -85:ba98 0002:00000260 -85:ba9b 0002:00000261 -85:ba9d 0002:00000262 -85:ba9f 0002:00000263 -85:baa2 0002:00000264 -85:baa4 0002:00000265 -85:baa7 0002:00000266 -85:baa9 0002:00000269 -85:baaa 0002:0000026a -85:baab 0002:0000026b -85:baac 0002:0000026c -85:baae 0002:0000026d -85:baaf 0002:0000026e -85:bab0 0002:0000026f -85:bab1 0002:00000274 -85:bab4 0002:00000275 -85:bab5 0002:00000276 -85:bab8 0002:00000277 -85:bab9 0002:00000278 -85:baba 0002:00000279 -85:babb 0002:0000027a -85:babc 0002:00000285 -85:babd 0002:00000286 -85:babf 0002:00000287 -85:bac2 0002:00000288 -85:bac4 0002:00000289 -85:bac7 0002:0000028a -85:bac9 0002:0000028d -85:baca 0002:0000028e -85:bacb 0002:0000028f -85:bacd 0002:00000290 -85:bace 0002:00000292 -85:bacf 0002:00000293 -85:bad1 0002:00000294 -85:bad4 0002:00000295 -85:bad6 0002:00000296 -85:bad9 0002:00000297 -85:badb 0002:00000298 -85:badc 0002:0000029a -85:badd 0002:0000029b -85:badf 0002:0000029c -85:bae2 0002:0000029d -85:bae4 0002:0000029e -85:bae7 0002:0000029f -85:bae9 0002:000002a0 -85:8246 0002:000002a5 -85:8249 0002:000002a6 -85:824b 0002:000002a7 -85:82f9 0002:000002ab +b8:8017 0002:0000006a +b8:801b 0002:0000006b +b8:801e 0002:0000006c +b8:8020 0002:0000006d +b8:8024 0002:0000006e +b8:8028 0002:0000006f +b8:802a 0002:00000070 +b8:802e 0002:00000071 +b8:8032 0002:00000072 +b8:8034 0002:00000074 +b8:8038 0002:00000075 +b8:803b 0002:00000078 +b8:803c 0002:00000079 +b8:803f 0002:0000007a +b8:8042 0002:0000007b +b8:8045 0002:0000007c +b8:8048 0002:0000007d +b8:8049 0002:0000007e +b8:804a 0002:0000007f +b8:804e 0002:00000080 +b8:804f 0002:00000082 +b8:8066 0002:00000086 +b8:8068 0002:00000087 +b8:8069 0002:00000088 +b8:806a 0002:00000089 +b8:806c 0002:0000008a +b8:806e 0002:0000008b +b8:8070 0002:0000008c +b8:8072 0002:0000008d +b8:8075 0002:0000008e +b8:8077 0002:0000008f +b8:807a 0002:00000090 +b8:807d 0002:00000091 +b8:807f 0002:00000092 +b8:8083 0002:00000094 +b8:8085 0002:00000095 +b8:8087 0002:00000096 +b8:8089 0002:00000097 +b8:808b 0002:00000098 +b8:808d 0002:00000099 +b8:808f 0002:0000009a +b8:8092 0002:0000009b +b8:8094 0002:0000009c +b8:8097 0002:0000009d +b8:809a 0002:0000009e +b8:809c 0002:0000009f +b8:80a0 0002:000000a1 +b8:80a3 0002:000000a2 +b8:80a7 0002:000000a3 +b8:80ab 0002:000000a4 +b8:80af 0002:000000a5 +b8:80b3 0002:000000a6 +b8:80b7 0002:000000a8 +b8:80bb 0002:000000b0 +b8:80be 0002:000000b1 +b8:80c1 0002:000000b3 +b8:80c2 0002:000000b4 +b8:80c3 0002:000000b5 +b8:80c7 0002:000000b6 +b8:80cb 0002:000000b7 +b8:80cd 0002:000000c4 +b8:80d1 0002:000000c5 +b8:80d4 0002:000000c6 +b8:80d6 0002:000000c7 +b8:80da 0002:000000c8 +b8:80dd 0002:000000c9 +b8:80df 0002:000000ce +b8:80e2 0002:000000cf +b8:80e6 0002:000000d0 +b8:80ea 0002:000000d3 +b8:80eb 0002:000000d3 +b8:80ec 0002:000000d3 +b8:80ed 0002:000000d3 +b8:80ee 0002:000000d4 +b8:80ef 0002:000000db +b8:80f0 0002:000000dc +b8:80f1 0002:000000dd +b8:80f2 0002:000000de +b8:80f3 0002:000000df +b8:80f4 0002:000000e1 +b8:80f7 0002:000000e2 +b8:80f8 0002:000000e3 +b8:80f9 0002:000000e4 +b8:80fa 0002:000000e5 +b8:80fc 0002:000000e7 +b8:80fd 0002:000000ee +b8:80fe 0002:000000ef +b8:80ff 0002:000000f0 +b8:8100 0002:000000f1 +b8:8102 0002:000000f3 +b8:8104 0002:000000f4 +b8:8105 0002:000000f5 +b8:8107 0002:000000f6 +b8:8109 0002:000000f8 +b8:810b 0002:000000f9 +b8:810c 0002:000000fa +b8:810d 0002:000000fb +b8:810f 0002:000000fd +b8:8111 0002:000000fe +b8:8113 0002:000000ff +b8:8114 0002:00000100 +b8:8115 0002:00000101 +b8:8117 0002:00000103 +b8:8118 0002:00000104 +b8:8119 0002:00000108 +b8:811d 0002:00000109 +b8:8121 0002:0000010a +b8:8125 0002:0000010b +b8:8129 0002:0000010c +b8:812d 0002:0000010d +b8:8131 0002:0000010e +b8:8135 0002:0000010f +b8:8139 0002:00000110 +b8:813d 0002:00000111 +b8:8141 0002:00000112 +b8:8142 0002:00000118 +b8:8143 0002:00000118 +b8:8144 0002:00000119 +b8:8145 0002:00000119 +b8:8146 0002:0000011a +b8:814a 0002:0000011b +b8:814d 0002:0000011b +b8:814e 0002:0000011c +b8:814f 0002:0000011d +b8:8153 0002:0000011e +b8:8154 0002:0000011f +b8:8158 0002:00000120 +b8:8159 0002:00000121 +b8:815d 0002:00000123 +b8:8161 0002:00000124 +b8:8162 0002:00000125 +b8:8166 0002:00000126 +b8:8167 0002:00000126 +b8:8168 0002:00000127 +b8:8169 0002:0000012c +b8:816a 0002:0000012c +b8:816b 0000:00000013 +b8:816d 0002:0000012f +b8:816e 0002:0000012f +b8:816f 0002:00000131 +b8:8170 0002:00000132 +b8:8173 0002:00000133 +b8:8174 0002:00000138 +b8:8175 0002:00000138 +b8:8176 0000:00000013 +b8:8178 0002:0000013a +b8:817c 0002:0000013b +b8:8180 0002:0000013c +b8:8182 0002:0000013e +b8:8183 0002:0000013e +b8:8184 0002:0000013f +b8:8185 0002:00000147 +b8:8189 0002:00000148 +b8:818b 0002:0000014e +b8:818d 0002:0000014f +b8:818f 0002:00000152 +b8:8192 0002:00000153 +b8:8194 0002:00000154 +b8:8197 0002:00000155 +b8:8199 0002:00000156 +b8:819c 0002:00000157 +b8:81a0 0002:00000158 +b8:81a2 0002:00000159 +b8:81a4 0002:0000015a +b8:81a6 0002:0000015b +b8:81a8 0002:0000015c +b8:81a9 0002:00000160 +b8:81aa 0002:00000160 +b8:81ab 0002:00000161 +b8:81ae 0002:00000162 +b8:81b0 0002:00000163 +b8:81b3 0002:00000164 +b8:81b5 0002:00000165 +b8:81b6 0002:00000166 +b8:81b7 0002:00000168 +b8:81ba 0002:00000169 +b8:81bc 0002:0000016a +b8:81bf 0002:0000016b +b8:81c0 0002:0000016c +b8:81c3 0002:0000016d +b8:81c4 0002:0000016d +b8:81c5 0002:0000016e +b8:81c9 0002:0000016f +b8:81ca 0002:00000170 +b8:81cd 0002:00000171 +b8:81d1 0002:00000172 +b8:81d3 0002:00000174 +b8:81d6 0002:00000175 +b8:81d8 0002:00000176 +b8:81db 0002:00000177 +b8:81dd 0002:00000179 +b8:81e1 0002:0000017b +b8:81e3 0002:0000017c +b8:81e5 0002:0000017d +b8:81e6 0002:0000017d +b8:81e7 0002:0000017e +b8:81e8 0002:00000189 +b8:81e9 0002:0000018a +b8:81ed 0002:0000018b +b8:81ee 0002:0000018c +b8:81f2 0002:0000018d +b8:81f3 0002:0000018f +b8:81f7 0002:00000190 +b8:81f8 0002:000001c2 +b8:81f9 0002:000001c2 +b8:81fa 0002:000001c5 +b8:81fe 0002:000001c6 +b8:8202 0002:000001c7 +b8:8204 0002:000001c9 +b8:8206 0002:000001c9 +b8:8207 0002:000001cc +b8:820b 0002:000001cd +b8:820d 0002:000001ce +b8:8211 0002:000001cf +b8:8213 0002:000001d0 +b8:8217 0002:000001d1 +b8:821a 0002:000001d2 +b8:821c 0002:000001d3 +b8:821e 0002:000001d4 +b8:8222 0002:000001d5 +b8:8224 0002:000001d6 +b8:8226 0002:000001d7 +b8:8229 0002:000001d8 +b8:822c 0002:000001d9 +b8:822e 0002:000001da +b8:8236 0002:000001de +b8:8237 0002:000001df +b8:8238 0002:000001e0 +b8:823c 0002:000001e3 +b8:8240 0002:000001e4 +b8:8244 0002:000001e5 +b8:8246 0002:000001e7 +b8:8247 0002:000001e8 +b8:8248 0002:000001e9 +b8:824a 0002:000001ee +b8:824b 0002:000001ef +b8:824f 0002:000001f2 +b8:8253 0002:000001f3 +b8:8257 0002:000001f4 +b8:825b 0002:000001f5 +b8:825c 0002:000001f9 +b8:825e 0002:000001fa +b8:8261 0002:000001fb +b8:8263 0002:000001fc +b8:8266 0002:000001ff +b8:826a 0002:00000200 +b8:826b 0002:00000201 +b8:826f 0002:00000203 +b8:8271 0002:00000206 +b8:8273 0002:00000207 +b8:8275 0002:00000208 +b8:8276 0002:00000208 +b8:8277 0002:00000209 +b8:8278 0002:0000020d +b8:8279 0002:0000020d +b8:827a 0002:0000020d +b8:827b 0002:0000020e +b8:827f 0002:0000020f +b8:8282 0002:0000020f +b8:8283 0002:00000211 +b8:8287 0002:00000212 +b8:8288 0002:00000213 +b8:828c 0002:00000214 +b8:828f 0002:00000215 +b8:8291 0002:00000217 +b8:8294 0002:0000021a +b8:8298 0002:0000021b +b8:829c 0002:0000021c +b8:829e 0002:0000021e +b8:82a2 0002:0000021f +b8:82a3 0002:00000221 +b8:82a7 0002:00000222 +b8:82a9 0002:00000225 +b8:82ad 0002:00000226 +b8:82b0 0002:00000227 +b8:82b2 0002:00000228 +b8:82b5 0002:0000022b +b8:82b6 0002:0000022c +b8:82b7 0002:0000022d +b8:82bb 0002:0000022e +b8:82bd 0002:00000231 +b8:82be 0002:00000231 +b8:82bf 0002:00000231 +b8:82c0 0002:00000232 +b8:82c1 0002:00000236 +b8:82c5 0002:00000237 +b8:82c9 0002:00000238 +b8:82cb 0002:00000239 +b8:82cf 0002:0000023a +b8:82d2 0002:0000023b +b8:82d4 0002:0000023c +b8:82d7 0002:0000023e +b8:82d8 0002:00000241 +b8:82dc 0002:00000243 +b8:82dd 0002:00000244 +b8:82de 0002:00000245 +b8:82df 0002:00000246 +b8:82e0 0002:00000247 +8b:914a 0002:0000024c +81:80f7 0002:0000024f +81:8027 0002:00000252 +82:8bb3 0002:00000255 +85:b9a3 0002:000002ef +85:b9a4 0002:000002ef +85:b9a5 0002:000002f2 +85:b9a7 0002:000002f3 +85:b9ad 0002:000002f3 +85:b9ae 0002:000002f4 +85:b9b1 0002:000002f5 +85:b9b2 0002:000002f6 +85:b9b3 0002:000002f6 +85:b9b4 0002:000002fa +85:b9b7 0002:000002fb +85:b9bb 0002:000002fc +85:b9bd 0002:000002fc +85:b9bf 0002:000002fd +85:b9c2 0002:000002fe +85:b9c4 0002:00000300 +85:b9c5 0002:00000301 +85:b9c7 0002:00000305 +85:b9ca 0002:00000307 +85:b9cd 0002:00000308 +85:b9cf 0002:00000309 +85:b9d1 0002:0000030a +85:b9d5 0002:0000030b +85:b9d7 0002:0000030c +85:b9d9 0002:0000030d +85:b9da 0002:0000030e +85:b9dc 0002:00000310 +85:b9df 0002:00000312 +85:b9e2 0002:00000312 +85:b9e3 0002:00000313 +85:b9e6 0002:00000315 +85:b9ea 0002:00000316 +85:b9ed 0002:00000317 +85:b9ee 0002:00000318 +85:b9ef 0002:00000318 +85:b9f0 0002:00000319 +85:b9f4 0002:0000031a +85:b9f5 0002:0000031b +85:b9f9 0002:0000031c +85:b9fb 0002:0000031d +85:b9fc 0002:0000031e +85:b9fd 0002:0000031f +85:ba00 0002:00000320 +85:ba02 0002:00000321 +85:ba04 0002:00000324 +85:ba05 0002:00000324 +85:ba06 0002:00000325 +85:ba09 0002:00000326 +85:ba8a 0002:00000334 +85:ba8c 0002:00000335 +85:ba8f 0002:00000336 +85:ba92 0002:00000337 +85:ba95 0002:0000033f +85:ba96 0002:00000340 +85:ba98 0002:00000341 +85:ba9b 0002:00000342 +85:ba9d 0002:00000343 +85:ba9f 0002:00000344 +85:baa2 0002:00000345 +85:baa4 0002:00000346 +85:baa7 0002:00000347 +85:baa9 0002:0000034a +85:baaa 0002:0000034b +85:baab 0002:0000034c +85:baac 0002:0000034d +85:baae 0002:0000034e +85:baaf 0002:0000034f +85:bab0 0002:00000350 +85:bab1 0002:00000355 +85:bab4 0002:00000356 +85:bab5 0002:00000357 +85:bab8 0002:00000358 +85:bab9 0002:00000359 +85:baba 0002:0000035a +85:babb 0002:0000035b +85:babc 0002:00000366 +85:babd 0002:00000367 +85:babf 0002:00000368 +85:bac2 0002:00000369 +85:bac4 0002:0000036a +85:bac7 0002:0000036b +85:bac9 0002:0000036e +85:baca 0002:0000036f +85:bacb 0002:00000370 +85:bacd 0002:00000371 +85:bace 0002:00000373 +85:bacf 0002:00000374 +85:bad1 0002:00000375 +85:bad4 0002:00000376 +85:bad6 0002:00000377 +85:bad9 0002:00000378 +85:badb 0002:00000379 +85:badc 0002:0000037b +85:badd 0002:0000037c +85:badf 0002:0000037d +85:bae2 0002:0000037e +85:bae4 0002:0000037f +85:bae7 0002:00000380 +85:bae9 0002:00000381 +85:8246 0002:00000386 +85:8249 0002:00000387 +85:824b 0002:00000388 +85:82f9 0002:0000038c b8:885c 0003:00000045 b8:885d 0003:00000045 b8:885e 0003:00000046 diff --git a/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json b/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json index 63198cde72..222548ba1e 100644 --- a/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json +++ b/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json @@ -4,7 +4,7 @@ "CLIPLEN_end": "85:990F", "CLIPLEN_no_multi": "85:990C", "CLIPSET": "85:FF1D", - "COLLECTTANK": "B8:80EF", + "COLLECTTANK": "B8:81E8", "MISCFX": "85:FF45", "NORMAL": "84:8BF2", "SETFX": "85:FF4E", @@ -22,6 +22,11 @@ "config_player_id": "CE:FF08", "config_remote_items": "CE:FF06", "config_sprite": "CE:FF02", + "copy_config_to_sram": "B8:8119", + "copy_memory": "B8:80FD", + "copy_memory_done": "B8:8117", + "copy_memory_even": "B8:8109", + "copy_memory_loop": "B8:810F", "h_item": "84:F894", "i_chozo_item": "84:F8AD", "i_hidden_item": "84:F8B4", @@ -30,11 +35,11 @@ "i_item_setup_shared_all_items": "B8:8878", "i_item_setup_shared_alwaysloaded": "B8:8883", "i_live_pickup": "84:FA79", - "i_live_pickup_multiworld": "B8:817F", - "i_live_pickup_multiworld_end": "B8:81C4", - "i_live_pickup_multiworld_local_item_or_offworld": "B8:819B", - "i_live_pickup_multiworld_own_item": "B8:81B0", - "i_live_pickup_multiworld_own_item1": "B8:81BC", + "i_live_pickup_multiworld": "B8:8278", + "i_live_pickup_multiworld_end": "B8:82BD", + "i_live_pickup_multiworld_local_item_or_offworld": "B8:8294", + "i_live_pickup_multiworld_own_item": "B8:82A9", + "i_live_pickup_multiworld_own_item1": "B8:82B5", "i_load_custom_graphics": "84:FA1E", "i_load_custom_graphics_all_items": "84:FA39", "i_load_custom_graphics_alwaysloaded": "84:FA49", @@ -69,22 +74,27 @@ "message_write_placeholders_loop": "85:B9CA", "message_write_placeholders_notfound": "85:B9DC", "message_write_placeholders_value_ok": "85:B9DF", - "mw_display_item_sent": "B8:8092", - "mw_handle_queue": "B8:80FF", - "mw_handle_queue_end": "B8:8178", - "mw_handle_queue_loop": "B8:8101", - "mw_handle_queue_new_remote_item": "B8:8151", - "mw_handle_queue_next": "B8:816D", - "mw_handle_queue_perform_receive": "B8:8163", - "mw_hook_main_game": "B8:81C8", + "mw_display_item_sent": "B8:818B", + "mw_handle_queue": "B8:81F8", + "mw_handle_queue_end": "B8:8271", + "mw_handle_queue_loop": "B8:81FA", + "mw_handle_queue_new_remote_item": "B8:824A", + "mw_handle_queue_next": "B8:8266", + "mw_handle_queue_perform_receive": "B8:825C", + "mw_hook_main_game": "B8:82C1", "mw_init": "B8:8011", - "mw_init_end": "B8:8044", + "mw_init_continuereset": "B8:8066", + "mw_init_end": "B8:80EA", "mw_init_memory": "B8:8000", - "mw_load_sram": "B8:8083", - "mw_receive_item": "B8:80B0", - "mw_receive_item_end": "B8:80E8", - "mw_save_sram": "B8:8070", - "mw_write_message": "B8:8049", + "mw_init_reset_sram": "B8:803B", + "mw_init_smstringdata": "B8:8051", + "mw_load_sram": "B8:8174", + "mw_load_sram_done": "B8:8182", + "mw_load_sram_setnewgame": "B8:8185", + "mw_receive_item": "B8:81A9", + "mw_receive_item_end": "B8:81E1", + "mw_save_sram": "B8:8169", + "mw_write_message": "B8:8142", "nonprog_item_eight_palette_indices": "84:F888", "offworld_graphics_data_item": "89:9200", "offworld_graphics_data_progression_item": "89:9100", @@ -109,7 +119,7 @@ "p_visible_item_end": "84:F96E", "p_visible_item_loop": "84:F95B", "p_visible_item_trigger": "84:F967", - "patch_load_multiworld": "B8:81DF", + "patch_load_multiworld": "B8:82D8", "perform_item_pickup": "84:FA7E", "plm_graphics_entry_offworld_item": "84:F886", "plm_graphics_entry_offworld_progression_item": "84:F87C", @@ -128,14 +138,24 @@ "start_item_data_reserve": "B8:C818", "update_graphic": "B8:C856", "v_item": "84:F890", + "write_repeated_memory": "B8:80EF", + "write_repeated_memory_loop": "B8:80F4", "ITEM_RAM": "7E:09A2", "SRAM_MW_ITEMS_RECV": "70:2000", - "SRAM_MW_ITEMS_RECV_RPTR": "70:2600", - "SRAM_MW_ITEMS_RECV_WPTR": "70:2602", - "SRAM_MW_ITEMS_RECV_SPTR": "70:2604", - "SRAM_MW_ITEMS_SENT_RPTR": "70:2680", - "SRAM_MW_ITEMS_SENT_WPTR": "70:2682", + "SRAM_MW_ITEMS_RECV_WCOUNT": "70:2602", + "ReceiveQueueCompletedCount_InRamThatGetsSavedToSaveSlot": "7e:d8ae", + "SRAM_MW_ITEMS_SENT_RCOUNT": "70:2680", + "SRAM_MW_ITEMS_SENT_WCOUNT": "70:2682", "SRAM_MW_ITEMS_SENT": "70:2700", - "SRAM_MW_INITIALIZED": "70:26fe", + "SRAM_MW_SM": "70:3000", + "SRAM_MW_ROMTITLE": "70:3015", + "SRAM_MW_SEEDINT": "70:3060", + "SRAM_MW_INITIALIZED": "70:3064", + "SRAM_MW_CONFIG_ENABLED": "70:3070", + "SRAM_MW_CONFIG_CUSTOM_SPRITE": "70:3072", + "SRAM_MW_CONFIG_DEATHLINK": "70:3074", + "SRAM_MW_CONFIG_REMOTE_ITEMS": "70:3076", + "SRAM_MW_CONFIG_PLAYER_ID": "70:3078", + "varia_seedint_location": "df:ff00", "CollectedItems": "7E:D86E" } \ No newline at end of file From 4c94bb0ad57a1c3b7ab7caaf8f0ab249ba787d00 Mon Sep 17 00:00:00 2001 From: strotlog <49286967+strotlog@users.noreply.github.com> Date: Fri, 26 Aug 2022 14:44:09 +0000 Subject: [PATCH 04/24] WebHost: sort game list case-insensitively again --- Utils.py | 4 ++-- WebHost.py | 2 +- WebHostLib/templates/supportedGames.html | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Utils.py b/Utils.py index c621e31c9a..4b2300a870 100644 --- a/Utils.py +++ b/Utils.py @@ -619,7 +619,7 @@ def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset def sorter(element: str) -> str: parts = element.split(maxsplit=1) if parts[0].lower() in ignore: - return parts[1] + return parts[1].lower() else: - return element + return element.lower() return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i)) diff --git a/WebHost.py b/WebHost.py index db802193a6..2ce0764214 100644 --- a/WebHost.py +++ b/WebHost.py @@ -104,7 +104,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]] for games in data: if 'Archipelago' in games['gameTitle']: generic_data = data.pop(data.index(games)) - sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"].lower()) + sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"]) json.dump(sorted_data, json_target, indent=2, ensure_ascii=False) return sorted_data diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index fe81463a46..82f6348db2 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -1,7 +1,7 @@ {% extends 'pageWrapper.html' %} {% block head %} - Player Settings + Supported Games {% endblock %} From cc8ce32c61b30df6d8ee5f3563218fb8ddf76d0c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 27 Aug 2022 09:21:47 +0200 Subject: [PATCH 05/24] Options: fix corner case where Toggle.value and Toggle.__int__ would be bool Which lead to a connect failure in Raft --- Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Options.py b/Options.py index a4f559a532..7eb108c99d 100644 --- a/Options.py +++ b/Options.py @@ -298,7 +298,7 @@ class Toggle(NumericOption): if type(data) == str: return cls.from_text(data) else: - return cls(data) + return cls(int(data)) @classmethod def get_option_name(cls, value): From 6d6111de2a91c66a73f81bc6ad4c6021ac9f2aee Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 27 Aug 2022 02:28:46 +0200 Subject: [PATCH 06/24] Launcher: add ModuleUpdate --- Launcher.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Launcher.py b/Launcher.py index 53032ea251..92f43cd26c 100644 --- a/Launcher.py +++ b/Launcher.py @@ -10,16 +10,20 @@ Scroll down to components= to add components to the launcher as well as setup.py import argparse -from os.path import isfile -import sys -from typing import Iterable, Sequence, Callable, Union, Optional -import subprocess import itertools -from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox,\ - is_windows, is_macos, is_linux -from shutil import which import shlex +import subprocess +import sys from enum import Enum, auto +from os.path import isfile +from shutil import which +from typing import Iterable, Sequence, Callable, Union, Optional + +import ModuleUpdate +ModuleUpdate.update() + +from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \ + is_windows, is_macos, is_linux def open_host_yaml(): From b1ffbc49c97334bfaff06fb4951a2277c30c362e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 28 Aug 2022 18:30:19 +0200 Subject: [PATCH 07/24] LttPAdjuster: fix GUI for invalid sprite files (#885) * LttPAdjuster: ignore invalid sprite files * LttPAdjuster: ignore .gitignore in sprites * LttPAdjuster: log and show message for invalid sprites * Alttp: set sprite.valid to False for bad zspr and apsprite ... ... when throwing exceptions Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- LttPAdjuster.py | 16 +++++++++- worlds/alttp/Rom.py | 74 +++++++++++++++++++++++++-------------------- 2 files changed, 57 insertions(+), 33 deletions(-) diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 3de6e3b13a..f516a20ec0 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -752,6 +752,7 @@ class SpriteSelector(): self.window['pady'] = 5 self.spritesPerRow = 32 self.all_sprites = [] + self.invalid_sprites = [] self.sprite_pool = spritePool def open_custom_sprite_dir(_evt): @@ -833,6 +834,13 @@ class SpriteSelector(): self.window.focus() tkinter_center_window(self.window) + if self.invalid_sprites: + invalid = sorted(self.invalid_sprites) + logging.warning(f"The following sprites are invalid: {', '.join(invalid)}") + msg = f"{invalid[0]} " + msg += f"and {len(invalid)-1} more are invalid" if len(invalid) > 1 else "is invalid" + messagebox.showerror("Invalid sprites detected", msg, parent=self.window) + def remove_from_sprite_pool(self, button, spritename): self.callback(("remove", spritename)) self.spritePoolButtons.buttons.remove(button) @@ -897,7 +905,13 @@ class SpriteSelector(): sprites = [] for file in os.listdir(path): - sprites.append((file, Sprite(os.path.join(path, file)))) + if file == '.gitignore': + continue + sprite = Sprite(os.path.join(path, file)) + if sprite.valid: + sprites.append((file, sprite)) + else: + self.invalid_sprites.append(file) sprites.sort(key=lambda s: str.lower(s[1].name or "").strip()) diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index dd5cc8c4dc..9ca3a355e1 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -34,7 +34,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts DeathMountain_texts, \ LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \ SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names -from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen +from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml from worlds.alttp.Items import ItemFactory, item_table, item_name_groups, progression_items from worlds.alttp.EntranceShuffle import door_addresses from worlds.alttp.Options import smallkey_shuffle @@ -551,18 +551,22 @@ class Sprite(): Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette def from_ap_sprite(self, filedata): - filedata = filedata.decode("utf-8-sig") - import yaml - obj = yaml.safe_load(filedata) - if obj["min_format_version"] > 1: - raise Exception("Sprite file requires an updated reader.") - self.author_name = obj["author"] - self.name = obj["name"] - if obj["data"]: # skip patching for vanilla content - data = bsdiff4.patch(Sprite.base_data, obj["data"]) - self.sprite = data[:self.sprite_size] - self.palette = data[self.sprite_size:self.palette_size] - self.glove_palette = data[self.sprite_size + self.palette_size:] + # noinspection PyBroadException + try: + obj = parse_yaml(filedata.decode("utf-8-sig")) + if obj["min_format_version"] > 1: + raise Exception("Sprite file requires an updated reader.") + self.author_name = obj["author"] + self.name = obj["name"] + if obj["data"]: # skip patching for vanilla content + data = bsdiff4.patch(Sprite.base_data, obj["data"]) + self.sprite = data[:self.sprite_size] + self.palette = data[self.sprite_size:self.palette_size] + self.glove_palette = data[self.sprite_size + self.palette_size:] + except Exception: + logger = logging.getLogger("apsprite") + logger.exception("Error parsing apsprite file") + self.valid = False @property def author_game_display(self) -> str: @@ -659,7 +663,7 @@ class Sprite(): @staticmethod def parse_zspr(filedata, expected_kind): - logger = logging.getLogger('ZSPR') + logger = logging.getLogger("ZSPR") headerstr = "<4xBHHIHIHH6x" headersize = struct.calcsize(headerstr) if len(filedata) < headersize: @@ -667,7 +671,7 @@ class Sprite(): version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from( headerstr, filedata) if version not in [1]: - logger.error('Error parsing ZSPR file: Version %g not supported', version) + logger.error("Error parsing ZSPR file: Version %g not supported", version) return None if kind != expected_kind: return None @@ -676,36 +680,42 @@ class Sprite(): stream.seek(headersize) def read_utf16le(stream): - "Decodes a null-terminated UTF-16_LE string of unknown size from a stream" + """Decodes a null-terminated UTF-16_LE string of unknown size from a stream""" raw = bytearray() while True: char = stream.read(2) - if char in [b'', b'\x00\x00']: + if char in [b"", b"\x00\x00"]: break raw += char - return raw.decode('utf-16_le') + return raw.decode("utf-16_le") - sprite_name = read_utf16le(stream) - author_name = read_utf16le(stream) - author_credits_name = stream.read().split(b"\x00", 1)[0].decode() + # noinspection PyBroadException + try: + sprite_name = read_utf16le(stream) + author_name = read_utf16le(stream) + author_credits_name = stream.read().split(b"\x00", 1)[0].decode() - # Ignoring the Author Rom name for the time being. + # Ignoring the Author Rom name for the time being. - real_csum = sum(filedata) % 0x10000 - if real_csum != csum or real_csum ^ 0xFFFF != icsum: - logger.warning('ZSPR file has incorrect checksum. It may be corrupted.') + real_csum = sum(filedata) % 0x10000 + if real_csum != csum or real_csum ^ 0xFFFF != icsum: + logger.warning("ZSPR file has incorrect checksum. It may be corrupted.") - sprite = filedata[sprite_offset:sprite_offset + sprite_size] - palette = filedata[palette_offset:palette_offset + palette_size] + sprite = filedata[sprite_offset:sprite_offset + sprite_size] + palette = filedata[palette_offset:palette_offset + palette_size] - if len(sprite) != sprite_size or len(palette) != palette_size: - logger.error('Error parsing ZSPR file: Unexpected end of file') + if len(sprite) != sprite_size or len(palette) != palette_size: + logger.error("Error parsing ZSPR file: Unexpected end of file") + return None + + return sprite, palette, sprite_name, author_name, author_credits_name + + except Exception: + logger.exception("Error parsing ZSPR file") return None - return (sprite, palette, sprite_name, author_name, author_credits_name) - def decode_palette(self): - "Returns the palettes as an array of arrays of 15 colors" + """Returns the palettes as an array of arrays of 15 colors""" def array_chunk(arr, size): return list(zip(*[iter(arr)] * size)) From 26aed9351ee73696e34d2172f66748508ec87840 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sun, 28 Aug 2022 20:58:26 -0700 Subject: [PATCH 08/24] Factorio: Fix a bug with single craft free samples. (#974) --- worlds/factorio/data/mod_template/control.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 63473808c6..51cd21e4da 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -249,6 +249,10 @@ script.on_event(defines.events.on_player_main_inventory_changed, update_player_e function add_samples(force, name, count) local function add_to_table(t) + if count <= 0 then + -- Fixes a bug with single craft, if a recipe gives 0 of a given item. + return + end t[name] = (t[name] or 0) + count end -- Add to global table of earned samples for future new players From 3eb9e7050f7dae3c8da5f53b38b1fb60b9385f24 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Mon, 29 Aug 2022 14:04:02 -0400 Subject: [PATCH 09/24] DKC3: Fix Wrinkly Softlock (#963) --- worlds/dkc3/Rom.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py index 90c4507e44..2bb5221a60 100644 --- a/worlds/dkc3/Rom.py +++ b/worlds/dkc3/Rom.py @@ -502,6 +502,10 @@ def patch_rom(world, rom, player, active_level_list): # Make Swanky free rom.write_byte(0x348C48, 0x00) + rom.write_bytes(0x34AB70, bytearray([0xEA, 0xEA])) + rom.write_bytes(0x34ABF7, bytearray([0xEA, 0xEA])) + rom.write_bytes(0x34ACD0, bytearray([0xEA, 0xEA])) + # Banana Bird Costs if world.goal[player] == "banana_bird_hunt": banana_bird_cost = math.floor(world.number_of_banana_birds[player] * world.percentage_of_banana_birds[player] / 100.0) From 45fb7353208f272822cde22719aa4d404d66a3e0 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 29 Aug 2022 17:16:13 -0500 Subject: [PATCH 10/24] Clients: allow games without datapackage (#978) --- CommonClient.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CommonClient.py b/CommonClient.py index f830035425..5af8e8cd88 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -345,6 +345,8 @@ class CommonContext: cache_package = Utils.persistent_load().get("datapackage", {}).get("games", {}) needed_updates: typing.Set[str] = set() for game in relevant_games: + if game not in remote_datepackage_versions: + continue remote_version: int = remote_datepackage_versions[game] if remote_version == 0: # custom datapackage for this game From 4a2a184db11ff97a32c1f15106db191460cd234a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 30 Aug 2022 17:12:33 +0200 Subject: [PATCH 11/24] Core: remove game-specific arguments from Generate (#971) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Generate.py | 11 +---------- Main.py | 1 - worlds/alttp/EntranceRandomizer.py | 2 -- worlds/alttp/__init__.py | 13 +++++++++---- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/Generate.py b/Generate.py index 1cad836345..d13a78b375 100644 --- a/Generate.py +++ b/Generate.py @@ -63,7 +63,7 @@ class PlandoSettings(enum.IntFlag): def __str__(self) -> str: if self.value: - return ", ".join((flag.name for flag in PlandoSettings if self.value & flag.value)) + return ", ".join(flag.name for flag in PlandoSettings if self.value & flag.value) return "Off" @@ -84,11 +84,6 @@ def mystery_argparse(): parser.add_argument('--seed', help='Define seed number to generate.', type=int) parser.add_argument('--multi', default=defaults["players"], type=lambda value: max(int(value), 1)) parser.add_argument('--spoiler', type=int, default=defaults["spoiler"]) - parser.add_argument('--lttp_rom', default=options["lttp_options"]["rom_file"], - help="Path to the 1.0 JP LttP Baserom.") # absolute, relative to cwd or relative to app path - parser.add_argument('--sm_rom', default=options["sm_options"]["rom_file"], - help="Path to the 1.0 JP SM Baserom.") - parser.add_argument('--enemizercli', default=resolve_path(defaults["enemizer_path"], local_path)) parser.add_argument('--outputpath', default=resolve_path(options["general_options"]["output_path"], user_path), help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd parser.add_argument('--race', action='store_true', default=defaults["race"]) @@ -183,10 +178,6 @@ def main(args=None, callback=ERmain): Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level) - erargs.lttp_rom = args.lttp_rom - erargs.sm_rom = args.sm_rom - erargs.enemizercli = args.enemizercli - settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None) for fname, yamls in weights_cache.items()} diff --git a/Main.py b/Main.py index 48095e06bd..acff74595a 100644 --- a/Main.py +++ b/Main.py @@ -70,7 +70,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world.required_medallions = args.required_medallions.copy() world.game = args.game.copy() world.player_name = args.name.copy() - world.enemizer = args.enemizercli world.sprite = args.sprite.copy() world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. diff --git a/worlds/alttp/EntranceRandomizer.py b/worlds/alttp/EntranceRandomizer.py index f1748b9a0f..47c36b6cde 100644 --- a/worlds/alttp/EntranceRandomizer.py +++ b/worlds/alttp/EntranceRandomizer.py @@ -212,9 +212,7 @@ def parse_arguments(argv, no_defaults=False): Alternatively, can be a ALttP Rom patched with a Link sprite that will be extracted. ''') - parser.add_argument('--gui', help='Launch the GUI', action='store_true') - parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core')) parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos', "singularity"]) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index e7f111c3b7..3e32584352 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -4,6 +4,7 @@ import random import threading import typing +import Utils from BaseClasses import Item, CollectionState, Tutorial from .Dungeons import create_dungeons from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect @@ -136,6 +137,10 @@ class ALTTPWorld(World): create_items = generate_itempool + enemizer_path: str = Utils.get_options()["generator"]["enemizer_path"] \ + if os.path.isabs(Utils.get_options()["generator"]["enemizer_path"]) \ + else Utils.local_path(Utils.get_options()["generator"]["enemizer_path"]) + def __init__(self, *args, **kwargs): self.dungeon_local_item_names = set() self.dungeon_specific_item_names = set() @@ -150,12 +155,12 @@ class ALTTPWorld(World): raise FileNotFoundError(rom_file) def generate_early(self): + if self.use_enemizer(): + check_enemizer(self.enemizer_path) + player = self.player world = self.world - if self.use_enemizer(): - check_enemizer(world.enemizer) - # system for sharing ER layouts self.er_seed = str(world.random.randint(0, 2 ** 64)) @@ -360,7 +365,7 @@ class ALTTPWorld(World): patch_rom(world, rom, player, use_enemizer) if use_enemizer: - patch_enemizer(world, player, rom, world.enemizer, output_directory) + patch_enemizer(world, player, rom, self.enemizer_path, output_directory) if world.is_race: patch_race_rom(rom, world, player) From 60d1a27079239bbcf08337e3322a4ab632480e2d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 30 Aug 2022 17:14:34 +0200 Subject: [PATCH 12/24] Subnautica: revamp aggressive creature scans (#966) * add forgotten aggressive creatures * fix logic requirements * added option to opt out of aggressive creature scans --- worlds/subnautica/Creatures.py | 29 +++++++++++++++++++++++++++-- worlds/subnautica/Locations.py | 7 ++++++- worlds/subnautica/Options.py | 15 ++++++++++++++- worlds/subnautica/Rules.py | 28 ++++++++++++++++++++++------ worlds/subnautica/__init__.py | 17 +++++++++-------- 5 files changed, 78 insertions(+), 18 deletions(-) diff --git a/worlds/subnautica/Creatures.py b/worlds/subnautica/Creatures.py index a9f5e850e1..687c3732a9 100644 --- a/worlds/subnautica/Creatures.py +++ b/worlds/subnautica/Creatures.py @@ -1,3 +1,4 @@ +import functools from typing import Dict, Set, List # EN Locale Creature Name to rough depth in meters found at @@ -58,13 +59,18 @@ all_creatures: Dict[str, int] = { aggressive: Set[str] = { "Cave Crawler", # is very easy without Stasis Rifle, but included for consistency "Crashfish", + "Biter", "Bleeder", + "Blighter", + "Blood Crawler", "Mesmer", "Reaper Leviathan", "Crabsquid", "Warper", "Crabsnake", "Ampeel", + "Stalker", + "Sand Shark", "Boneshark", "Lava Lizard", "Sea Dragon Leviathan", @@ -94,6 +100,25 @@ creature_locations: Dict[str, int] = { creature + suffix: creature_id for creature_id, creature in enumerate(all_creatures, start=34000) } -all_creatures_presorted: List[str] = sorted(all_creatures) -all_creatures_presorted_without_containment = [name for name in all_creatures_presorted if name not in containment] +class Definitions: + """Only compute lists if needed and then cache them.""" + + @functools.cached_property + def all_creatures_presorted(self) -> List[str]: + return sorted(all_creatures) + + @functools.cached_property + def all_creatures_presorted_without_containment(self) -> List[str]: + return [name for name in self.all_creatures_presorted if name not in containment] + + @functools.cached_property + def all_creatures_presorted_without_stasis(self) -> List[str]: + return [name for name in self.all_creatures_presorted if name not in aggressive or name in hatchable] + + @functools.cached_property + def all_creatures_presorted_without_aggressive(self) -> List[str]: + return [name for name in self.all_creatures_presorted if name not in aggressive] + +# only singleton needed +Definitions: Definitions = Definitions() diff --git a/worlds/subnautica/Locations.py b/worlds/subnautica/Locations.py index 3effd1eac3..2dfeaf3bf3 100644 --- a/worlds/subnautica/Locations.py +++ b/worlds/subnautica/Locations.py @@ -15,7 +15,12 @@ class LocationDict(TypedDict, total=False): need_propulsion_cannon: bool -events: List[str] = ["Neptune Launch", "Disable Quarantine", "Full Infection", "Repair Aurora Drive"] +events: List[str] = [ + "Neptune Launch", + "Disable Quarantine", + "Full Infection", + "Repair Aurora Drive", +] location_table: Dict[int, LocationDict] = { 33000: {'can_slip_through': False, diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index f68e12d2c0..57bd23fdb7 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,7 +1,7 @@ import typing from Options import Choice, Range, DeathLink -from .Creatures import all_creatures +from .Creatures import all_creatures, Definitions class ItemPool(Choice): @@ -46,14 +46,27 @@ class AggressiveScanLogic(Choice): Containment: Removes Stasis Rifle as expected solution and expects Alien Containment instead. Either: Creatures may be expected to be scanned via Stasis Rifle or Containment, whichever is found first. None: Aggressive Creatures are assumed to not need any tools to scan. + Removed: No Creatures needing Stasis or Containment will be in the pool at all. Note: Containment, Either and None adds Cuddlefish as an option for scans. + Note: Stasis, Either and None adds unhatchable aggressive species, such as Warper. Note: This is purely a logic expectation, and does not affect gameplay, only placement.""" display_name = "Aggressive Creature Scan Logic" option_stasis = 0 option_containment = 1 option_either = 2 option_none = 3 + option_removed = 4 + + def get_pool(self) -> typing.List[str]: + if self == self.option_removed: + return Definitions.all_creatures_presorted_without_aggressive + elif self == self.option_stasis: + return Definitions.all_creatures_presorted_without_containment + elif self == self.option_containment: + return Definitions.all_creatures_presorted_without_stasis + else: + return Definitions.all_creatures_presorted class SubnauticaDeathLink(DeathLink): diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py index 20c6a35c84..8925f1e829 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/Rules.py @@ -1,8 +1,8 @@ -from typing import TYPE_CHECKING, Dict, Callable +from typing import TYPE_CHECKING, Dict, Callable, Optional from worlds.generic.Rules import set_rule, add_rule from .Locations import location_table, LocationDict -from .Creatures import all_creatures, aggressive, suffix +from .Creatures import all_creatures, aggressive, suffix, hatchable, containment from .Options import AggressiveScanLogic import math @@ -258,6 +258,15 @@ def set_creature_rule(world, player: int, creature_name: str) -> "Location": return location +def get_aggression_rule(option: AggressiveScanLogic, creature_name: str) -> \ + Optional[Callable[["CollectionState", int], bool]]: + """Get logic rule for a creature scan location.""" + if creature_name not in hatchable and option != option.option_none: # can only be done via stasis + return has_stasis_rifle + # otherwise allow option preference + return aggression_rules.get(option.value, None) + + aggression_rules: Dict[int, Callable[["CollectionState", int], bool]] = { AggressiveScanLogic.option_stasis: has_stasis_rifle, AggressiveScanLogic.option_containment: has_containment, @@ -274,14 +283,21 @@ def set_rules(subnautica_world: "SubnauticaWorld"): set_location_rule(world, player, loc) if subnautica_world.creatures_to_scan: - aggressive_rule = aggression_rules.get(world.creature_scan_logic[player], None) + option = world.creature_scan_logic[player] + for creature_name in subnautica_world.creatures_to_scan: location = set_creature_rule(world, player, creature_name) - if creature_name in aggressive and aggressive_rule: - add_rule(location, lambda state: aggressive_rule(state, player)) + if creature_name in containment: # there is no other way, hard-required containment + add_rule(location, lambda state: has_containment(state, player)) + elif creature_name in aggressive: + rule = get_aggression_rule(option, creature_name) + if rule: + add_rule(location, + lambda state, loc_rule=get_aggression_rule(option, creature_name): loc_rule(state, player)) # Victory locations - set_rule(world.get_location("Neptune Launch", player), lambda state: + set_rule(world.get_location("Neptune Launch", player), + lambda state: get_max_depth(state, player) >= 1444 and has_mobile_vehicle_bay(state, player) and state.has("Neptune Launch Platform", player) and diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 806c1b195e..a4447ccbc1 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -52,14 +52,15 @@ class SubnauticaWorld(World): self.create_item("Seaglide Fragment"), self.create_item("Seaglide Fragment") ] - if self.world.creature_scan_logic[self.player] == Options.AggressiveScanLogic.option_stasis: - valid_creatures = Creatures.all_creatures_presorted_without_containment - self.world.creature_scans[self.player].value = min(len( - Creatures.all_creatures_presorted_without_containment), - self.world.creature_scans[self.player].value) - else: - valid_creatures = Creatures.all_creatures_presorted - self.creatures_to_scan = self.world.random.sample(valid_creatures, + scan_option: Options.AggressiveScanLogic = self.world.creature_scan_logic[self.player] + creature_pool = scan_option.get_pool() + + self.world.creature_scans[self.player].value = min( + len(creature_pool), + self.world.creature_scans[self.player].value + ) + + self.creatures_to_scan = self.world.random.sample(creature_pool, self.world.creature_scans[self.player].value) def create_regions(self): From 2a7babce6871797a1f037be4e5a8bd828fe2c505 Mon Sep 17 00:00:00 2001 From: strotlog <49286967+strotlog@users.noreply.github.com> Date: Tue, 30 Aug 2022 08:16:21 -0700 Subject: [PATCH 13/24] SM+SMZ3: don't abandon checks that happen while disconnected from AP (#946) --- SNIClient.py | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/SNIClient.py b/SNIClient.py index aad231691b..c97d0d6c0d 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -149,8 +149,8 @@ class Context(CommonContext): def event_invalid_slot(self): if self.snes_socket is not None and not self.snes_socket.closed: asyncio.create_task(self.snes_socket.close()) - raise Exception('Invalid ROM detected, ' - 'please verify that you have loaded the correct rom and reconnect your snes (/snes)') + raise Exception("Invalid ROM detected, " + "please verify that you have loaded the correct rom and reconnect your snes (/snes)") async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -158,7 +158,7 @@ class Context(CommonContext): if self.rom is None: self.awaiting_rom = True snes_logger.info( - 'No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)') + "No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)") return self.awaiting_rom = False self.auth = self.rom @@ -262,7 +262,7 @@ async def deathlink_kill_player(ctx: Context): SNES_RECONNECT_DELAY = 5 -# LttP +# FXPAK Pro protocol memory mapping used by SNI ROM_START = 0x000000 WRAM_START = 0xF50000 WRAM_SIZE = 0x20000 @@ -293,21 +293,24 @@ SHOP_LEN = (len(Shops.shop_table) * 3) + 5 DEATH_LINK_ACTIVE_ADDR = ROMNAME_START + 0x15 # 1 byte # SM -SM_ROMNAME_START = 0x007FC0 +SM_ROMNAME_START = ROM_START + 0x007FC0 SM_INGAME_MODES = {0x07, 0x09, 0x0b} SM_ENDGAME_MODES = {0x26, 0x27} SM_DEATH_MODES = {0x15, 0x17, 0x18, 0x19, 0x1A} -SM_RECV_PROGRESS_ADDR = SRAM_START + 0x2000 # 2 bytes -SM_RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte -SM_RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte +# RECV and SEND are from the gameplay's perspective: SNIClient writes to RECV queue and reads from SEND queue +SM_RECV_QUEUE_START = SRAM_START + 0x2000 +SM_RECV_QUEUE_WCOUNT = SRAM_START + 0x2602 +SM_SEND_QUEUE_START = SRAM_START + 0x2700 +SM_SEND_QUEUE_RCOUNT = SRAM_START + 0x2680 +SM_SEND_QUEUE_WCOUNT = SRAM_START + 0x2682 SM_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x277f04 # 1 byte SM_REMOTE_ITEM_FLAG_ADDR = ROM_START + 0x277f06 # 1 byte # SMZ3 -SMZ3_ROMNAME_START = 0x00FFC0 +SMZ3_ROMNAME_START = ROM_START + 0x00FFC0 SMZ3_INGAME_MODES = {0x07, 0x09, 0x0b} SMZ3_ENDGAME_MODES = {0x26, 0x27} @@ -1083,6 +1086,9 @@ async def game_watcher(ctx: Context): if ctx.awaiting_rom: await ctx.server_auth(False) + elif ctx.server is None: + snes_logger.warning("ROM detected but no active multiworld server connection. " + + "Connect using command: /connect server:port") if ctx.auth and ctx.auth != ctx.rom: snes_logger.warning("ROM change detected, please reconnect to the multiworld server") @@ -1159,6 +1165,9 @@ async def game_watcher(ctx: Context): await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}]) await track_locations(ctx, roomid, roomdata) elif ctx.game == GAME_SM: + if ctx.server is None or ctx.slot is None: + # not successfully connected to a multiworld server, cannot process the game sending items + continue gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1) if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time(): currently_dead = gamemode[0] in SM_DEATH_MODES @@ -1169,22 +1178,22 @@ async def game_watcher(ctx: Context): ctx.finished_game = True continue - data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x680, 4) + data = await snes_read(ctx, SM_SEND_QUEUE_RCOUNT, 4) if data is None: continue recv_index = data[0] | (data[1] << 8) - recv_item = data[2] | (data[3] << 8) + recv_item = data[2] | (data[3] << 8) # this is actually SM_SEND_QUEUE_WCOUNT while (recv_index < recv_item): itemAdress = recv_index * 8 - message = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x700 + itemAdress, 8) + message = await snes_read(ctx, SM_SEND_QUEUE_START + itemAdress, 8) # worldId = message[0] | (message[1] << 8) # unused # itemId = message[2] | (message[3] << 8) # unused itemIndex = (message[4] | (message[5] << 8)) >> 3 recv_index += 1 - snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x680, + snes_buffered_write(ctx, SM_SEND_QUEUE_RCOUNT, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) from worlds.sm.Locations import locations_start_id @@ -1196,12 +1205,11 @@ async def game_watcher(ctx: Context): 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, SM_RECV_PROGRESS_ADDR + 0x600, 4) + data = await snes_read(ctx, SM_RECV_QUEUE_WCOUNT, 2) if data is None: continue - # recv_itemOutPtr = data[0] | (data[1] << 8) # unused - itemOutPtr = data[2] | (data[3] << 8) + itemOutPtr = data[0] | (data[1] << 8) from worlds.sm.Items import items_start_id from worlds.sm.Locations import locations_start_id @@ -1214,10 +1222,10 @@ async def game_watcher(ctx: Context): locationId = 0x00 #backward compat playerID = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0 - snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + itemOutPtr * 4, bytes( + snes_buffered_write(ctx, SM_RECV_QUEUE_START + itemOutPtr * 4, bytes( [playerID & 0xFF, (playerID >> 8) & 0xFF, itemId & 0xFF, locationId & 0xFF])) itemOutPtr += 1 - snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x602, + snes_buffered_write(ctx, SM_RECV_QUEUE_WCOUNT, bytes([itemOutPtr & 0xFF, (itemOutPtr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( color(ctx.item_names[item.item], 'red', 'bold'), @@ -1225,6 +1233,9 @@ async def game_watcher(ctx: Context): ctx.location_names[item.location], itemOutPtr, len(ctx.items_received))) await snes_flush_writes(ctx) elif ctx.game == GAME_SMZ3: + if ctx.server is None or ctx.slot is None: + # not successfully connected to a multiworld server, cannot process the game sending items + continue currentGame = await snes_read(ctx, SRAM_START + 0x33FE, 2) if (currentGame is not None): if (currentGame[0] != 0): From a753905ee4536e4c28ef25dd0b230fe03fa683b1 Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Tue, 30 Aug 2022 11:54:40 -0700 Subject: [PATCH 14/24] OoT bug fixes (#955) * OoT: fix shop patching crash due to Item changes * OoT: more informative failure in triforce piece replacement * OoT: in triforce hunt, remove ganon BK from pool and lock the door * OoT: no longer store trap information on the item --- worlds/oot/ItemPool.py | 4 ++++ worlds/oot/Items.py | 1 - worlds/oot/Options.py | 4 ++-- worlds/oot/Patches.py | 30 ++++++++++++++---------------- worlds/oot/__init__.py | 11 ++++++++--- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py index 301c502a7e..12c9c26292 100644 --- a/worlds/oot/ItemPool.py +++ b/worlds/oot/ItemPool.py @@ -1388,6 +1388,10 @@ def get_pool_core(world): remove_junk_pool = list(remove_junk_pool) + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)', 'Ice Trap'] junk_candidates = [item for item in pool if item in remove_junk_pool] + if len(pending_junk_pool) > len(junk_candidates): + excess = len(pending_junk_pool) - len(junk_candidates) + if world.triforce_hunt: + raise RuntimeError(f"Items in the pool for player {world.player} exceed locations. Add {excess} location(s) or remove {excess} triforce piece(s).") while pending_junk_pool: pending_item = pending_junk_pool.pop() if not junk_candidates: diff --git a/worlds/oot/Items.py b/worlds/oot/Items.py index 31e6c31f62..06164091a7 100644 --- a/worlds/oot/Items.py +++ b/worlds/oot/Items.py @@ -49,7 +49,6 @@ class OOTItem(Item): self.type = type self.index = index self.special = special or {} - self.looks_like_item = None self.price = special.get('price', None) if special else None self.internal = False diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index 50b6c26c86..ea9a8160fb 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -158,12 +158,12 @@ class TriforceGoal(Range): """Number of Triforce pieces required to complete the game.""" display_name = "Required Triforce Pieces" range_start = 1 - range_end = 100 + range_end = 80 default = 20 class ExtraTriforces(Range): - """Percentage of additional Triforce pieces in the pool, separate from the item pool setting.""" + """Percentage of additional Triforce pieces in the pool. With high numbers, you may need to randomize additional locations to have enough items.""" display_name = "Percentage of Extra Triforce Pieces" range_start = 0 range_end = 100 diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index 7bf31c4f7a..322d2d838a 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -1844,7 +1844,7 @@ def write_rom_item(rom, item_id, item): def get_override_table(world): - return list(filter(lambda val: val != None, map(partial(get_override_entry, world.player), world.world.get_filled_locations(world.player)))) + return list(filter(lambda val: val != None, map(partial(get_override_entry, world), world.world.get_filled_locations(world.player)))) override_struct = struct.Struct('>xBBBHBB') # match override_t in get_items.c @@ -1852,10 +1852,10 @@ def get_override_table_bytes(override_table): return b''.join(sorted(itertools.starmap(override_struct.pack, override_table))) -def get_override_entry(player_id, location): +def get_override_entry(ootworld, location): scene = location.scene default = location.default - player_id = 0 if player_id == location.item.player else min(location.item.player, 255) + player_id = 0 if ootworld.player == location.item.player else min(location.item.player, 255) if location.item.game != 'Ocarina of Time': # This is an AP sendable. It's guaranteed to not be None. if location.item.advancement: @@ -1869,7 +1869,7 @@ def get_override_entry(player_id, location): if location.item.trap: item_id = 0x7C # Ice Trap ID, to get "X is a fool" message - looks_like_item_id = location.item.looks_like_item.index + looks_like_item_id = ootworld.trap_appearances[location.address].index else: looks_like_item_id = 0 @@ -2091,7 +2091,8 @@ def get_locked_doors(rom, world): return [0x00D4 + scene * 0x1C + 0x04 + flag_byte, flag_bits] # If boss door, set the door's unlock flag - if (world.shuffle_bosskeys == 'remove' and scene != 0x0A) or (world.shuffle_ganon_bosskey == 'remove' and scene == 0x0A): + if (world.shuffle_bosskeys == 'remove' and scene != 0x0A) or ( + world.shuffle_ganon_bosskey == 'remove' and scene == 0x0A and not world.triforce_hunt): if actor_id == 0x002E and actor_type == 0x05: return [0x00D4 + scene * 0x1C + 0x04 + flag_byte, flag_bits] @@ -2109,23 +2110,20 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F rom.write_int16(location.address1, location.item.index) else: if location.item.trap: - item_display = location.item.looks_like_item - elif location.item.game != "Ocarina of Time": - item_display = location.item - if location.item.advancement: - item_display.index = 0xCB - else: - item_display.index = 0xCC - item_display.special = {} + item_display = world.trap_appearances[location.address] else: item_display = location.item # bottles in shops should look like empty bottles # so that that are different than normal shop refils - if 'shop_object' in item_display.special: - rom_item = read_rom_item(rom, item_display.special['shop_object']) + if location.item.trap or location.item.game == "Ocarina of Time": + if 'shop_object' in item_display.special: + rom_item = read_rom_item(rom, item_display.special['shop_object']) + else: + rom_item = read_rom_item(rom, item_display.index) else: - rom_item = read_rom_item(rom, item_display.index) + display_index = 0xCB if location.item.advancement else 0xCC + rom_item = read_rom_item(rom, display_index) shop_objs.add(rom_item['object_id']) shop_id = world.current_shop_id diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index b4635ad77f..a9b7d5a1b6 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -178,6 +178,10 @@ class OOTWorld(World): if self.skip_child_zelda: self.shuffle_weird_egg = False + # Ganon boss key should not be in itempool in triforce hunt + if self.triforce_hunt: + self.shuffle_ganon_bosskey = 'remove' + # Determine skipped trials in GT # This needs to be done before the logic rules in GT are parsed trial_list = ['Forest', 'Fire', 'Water', 'Spirit', 'Shadow', 'Light'] @@ -803,9 +807,10 @@ class OOTWorld(World): with i_o_limiter: # Make traps appear as other random items - ice_traps = [loc.item for loc in self.get_locations() if loc.item.trap] - for trap in ice_traps: - trap.looks_like_item = self.create_item(self.world.slot_seeds[self.player].choice(self.fake_items).name) + trap_location_ids = [loc.address for loc in self.get_locations() if loc.item.trap] + self.trap_appearances = {} + for loc_id in trap_location_ids: + self.trap_appearances[loc_id] = self.create_item(self.world.slot_seeds[self.player].choice(self.fake_items).name) # Seed hint RNG, used for ganon text lines also self.hint_rng = self.world.slot_seeds[self.player] From fcfc2c2e100189e363cff2f47a30b0bbc8154c57 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 31 Aug 2022 00:10:18 +0200 Subject: [PATCH 15/24] WebHost: fix local_path on python 3.8 (#981) * WebHost: fix local_path on python 3.8 `__file__` is relative in 3.8, so `os.path.dirname(__file__)` ends up being an empty string breaking calls to `local_path()` (without arguments) * WebHost: add comment to local_path override --- WebHost.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHost.py b/WebHost.py index 2ce0764214..4c07e8b185 100644 --- a/WebHost.py +++ b/WebHost.py @@ -12,7 +12,7 @@ ModuleUpdate.update() # in case app gets imported by something like gunicorn import Utils -Utils.local_path.cached_path = os.path.dirname(__file__) +Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8 from WebHostLib import register, app as raw_app from waitress import serve From 8da1cfeeb752b1aee83ee1ec53e9da8493bc544e Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Wed, 31 Aug 2022 00:14:17 -0400 Subject: [PATCH 16/24] SM: remove events from data package (#973) --- SNIClient.py | 6 +++--- worlds/sm/Items.py | 14 -------------- worlds/sm/Locations.py | 14 -------------- worlds/sm/__init__.py | 17 ++++++++--------- 4 files changed, 11 insertions(+), 40 deletions(-) delete mode 100644 worlds/sm/Items.py delete mode 100644 worlds/sm/Locations.py diff --git a/SNIClient.py b/SNIClient.py index c97d0d6c0d..3d90fafc17 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -1196,7 +1196,7 @@ async def game_watcher(ctx: Context): snes_buffered_write(ctx, SM_SEND_QUEUE_RCOUNT, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) - from worlds.sm.Locations import locations_start_id + from worlds.sm import locations_start_id location_id = locations_start_id + itemIndex ctx.locations_checked.add(location_id) @@ -1211,8 +1211,8 @@ async def game_watcher(ctx: Context): itemOutPtr = data[0] | (data[1] << 8) - from worlds.sm.Items import items_start_id - from worlds.sm.Locations import locations_start_id + from worlds.sm import items_start_id + from worlds.sm import locations_start_id if itemOutPtr < len(ctx.items_received): item = ctx.items_received[itemOutPtr] itemId = item.item - items_start_id diff --git a/worlds/sm/Items.py b/worlds/sm/Items.py deleted file mode 100644 index ff8970b64d..0000000000 --- a/worlds/sm/Items.py +++ /dev/null @@ -1,14 +0,0 @@ -from worlds.sm.variaRandomizer.rando.Items import ItemManager - -items_start_id = 83000 - -def gen_special_id(): - special_id_value_start = 32 - while True: - yield special_id_value_start - special_id_value_start += 1 - -gen_run = gen_special_id() - -lookup_id_to_name = dict((items_start_id + (value.Id if value.Id != None else next(gen_run)), value.Name) for key, value in ItemManager.Items.items()) -lookup_name_to_id = {item_name: item_id for item_id, item_name in lookup_id_to_name.items()} \ No newline at end of file diff --git a/worlds/sm/Locations.py b/worlds/sm/Locations.py deleted file mode 100644 index 4e80ab00e6..0000000000 --- a/worlds/sm/Locations.py +++ /dev/null @@ -1,14 +0,0 @@ -from worlds.sm.variaRandomizer.graph.location import locationsDict - -locations_start_id = 82000 - -def gen_boss_id(): - boss_id_value_start = 256 - while True: - yield boss_id_value_start - boss_id_value_start += 1 - -gen_run = gen_boss_id() - -lookup_id_to_name = dict((locations_start_id + (value.Id if value.Id != None else next(gen_run)), key) for key, value in locationsDict.items()) -lookup_name_to_id = {location_name: location_id for location_id, location_name in lookup_id_to_name.items()} \ No newline at end of file diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 5da1c40f75..fbf3825e0f 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -11,8 +11,6 @@ from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils logger = logging.getLogger("Super Metroid") -from .Locations import lookup_name_to_id as locations_lookup_name_to_id -from .Items import lookup_name_to_id as items_lookup_name_to_id from .Regions import create_regions from .Rules import set_rules, add_entrance_rule from .Options import sm_options @@ -68,6 +66,8 @@ class SMWeb(WebWorld): ["Farrak Kilhn"] )] +locations_start_id = 82000 +items_start_id = 83000 class SMWorld(World): """ @@ -78,12 +78,11 @@ class SMWorld(World): game: str = "Super Metroid" topology_present = True - data_version = 1 + data_version = 2 option_definitions = sm_options - item_names: Set[str] = frozenset(items_lookup_name_to_id) - location_names: Set[str] = frozenset(locations_lookup_name_to_id) - item_name_to_id = items_lookup_name_to_id - location_name_to_id = locations_lookup_name_to_id + + item_name_to_id = {value.Name: items_start_id + value.Id for key, value in ItemManager.Items.items() if value.Id != None} + location_name_to_id = {key: locations_start_id + value.Id for key, value in locationsDict.items() if value.Id != None} web = SMWeb() remote_items: bool = False @@ -701,8 +700,8 @@ class SMWorld(World): dest.Name) for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions if src.Boss])) def create_locations(self, player: int): - for name, id in locations_lookup_name_to_id.items(): - self.locations[name] = SMLocation(player, name, id) + for name in locationsDict: + self.locations[name] = SMLocation(player, name, self.location_name_to_id.get(name, None)) def create_region(self, world: MultiWorld, player: int, name: str, locations=None, exits=None): From c617bba95993a8b7099a678b59ff1062d932266d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 31 Aug 2022 20:55:15 +0200 Subject: [PATCH 17/24] SC2: client revamp (#967) SC2 client now relies almost entirely on the map file and server for the locations and just facilitates them, should make it significantly more resilient to objectives being added or removed * SC2: fix client crash on printjson messages with more [ than ] * SC2: move text to queue, that actually clears memory * SC2: Announce which mission is being loaded Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- CommonClient.py | 7 +- Starcraft2Client.py | 431 ++++++++++++++------------------- Utils.py | 2 +- worlds/sc2wol/MissionTables.py | 4 +- worlds/sc2wol/__init__.py | 1 + 5 files changed, 187 insertions(+), 258 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 5af8e8cd88..574da16f2a 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -152,8 +152,9 @@ class CommonContext: # locations locations_checked: typing.Set[int] # local state locations_scouted: typing.Set[int] - missing_locations: typing.Set[int] + missing_locations: typing.Set[int] # server state checked_locations: typing.Set[int] # server state + server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations locations_info: typing.Dict[int, NetworkItem] # internals @@ -184,8 +185,9 @@ class CommonContext: self.locations_checked = set() # local state self.locations_scouted = set() self.items_received = [] - self.missing_locations = set() + self.missing_locations = set() # server state self.checked_locations = set() # server state + self.server_locations = set() # all locations the server knows of, missing_location | checked_locations self.locations_info = {} self.input_queue = asyncio.Queue() @@ -634,6 +636,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict): # when /missing is used for the client side view of what is missing. ctx.missing_locations = set(args["missing_locations"]) ctx.checked_locations = set(args["checked_locations"]) + ctx.server_locations = ctx.missing_locations | ctx. checked_locations elif cmd == 'ReceivedItems': start_index = args["index"] diff --git a/Starcraft2Client.py b/Starcraft2Client.py index dc63e9a456..b8f6086914 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -1,31 +1,31 @@ from __future__ import annotations -import multiprocessing -import logging import asyncio +import copy +import ctypes +import logging +import multiprocessing import os.path +import re +import sys +import typing +import queue +from pathlib import Path import nest_asyncio import sc2 - -from sc2.main import run_game -from sc2.data import Race from sc2.bot_ai import BotAI +from sc2.data import Race +from sc2.main import run_game from sc2.player import Bot -from worlds.sc2wol.Regions import MissionInfo -from worlds.sc2wol.MissionTables import lookup_id_to_mission +from MultiServer import mark_raw +from Utils import init_logging, is_windows +from worlds.sc2wol import SC2WoLWorld from worlds.sc2wol.Items import lookup_id_to_name, item_table from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET -from worlds.sc2wol import SC2WoLWorld - -from pathlib import Path -import re -from MultiServer import mark_raw -import ctypes -import sys - -from Utils import init_logging, is_windows +from worlds.sc2wol.MissionTables import lookup_id_to_mission +from worlds.sc2wol.Regions import MissionInfo if __name__ == "__main__": init_logging("SC2Client", exception_logger="Client") @@ -35,10 +35,12 @@ sc2_logger = logging.getLogger("Starcraft2") import colorama -from NetUtils import * +from NetUtils import ClientStatus, RawJSONtoTextParser from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser nest_asyncio.apply() +max_bonus: int = 8 +victory_modulo: int = 100 class StarcraftClientProcessor(ClientCommandProcessor): @@ -98,13 +100,13 @@ class StarcraftClientProcessor(ClientCommandProcessor): def _cmd_available(self) -> bool: """Get what missions are currently available to play""" - request_available_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui) + request_available_missions(self.ctx) return True def _cmd_unfinished(self) -> bool: """Get what missions are currently available to play and have not had all locations checked""" - request_unfinished_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui, self.ctx) + request_unfinished_missions(self.ctx) return True @mark_raw @@ -125,18 +127,19 @@ class SC2Context(CommonContext): items_handling = 0b111 difficulty = -1 all_in_choice = 0 - mission_req_table = None - items_rec_to_announce = [] - rec_announce_pos = 0 - items_sent_to_announce = [] - sent_announce_pos = 0 - announcements = [] - announcement_pos = 0 + mission_req_table: typing.Dict[str, MissionInfo] = {} + announcements = queue.Queue() sc2_run_task: typing.Optional[asyncio.Task] = None - missions_unlocked = False + missions_unlocked: bool = False # allow launching missions ignoring requirements current_tooltip = None last_loc_list = None difficulty_override = -1 + mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {} + raw_text_parser: RawJSONtoTextParser + + def __init__(self, *args, **kwargs): + super(SC2Context, self).__init__(*args, **kwargs) + self.raw_text_parser = RawJSONtoTextParser(self) async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -149,30 +152,32 @@ class SC2Context(CommonContext): self.difficulty = args["slot_data"]["game_difficulty"] self.all_in_choice = args["slot_data"]["all_in_map"] slot_req_table = args["slot_data"]["mission_req"] - self.mission_req_table = {} - # Compatibility for 0.3.2 server data. - if "category" not in next(iter(slot_req_table)): - for i, mission_data in enumerate(slot_req_table.values()): - mission_data["category"] = wol_default_categories[i] - for mission in slot_req_table: - self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission]) + self.mission_req_table = { + mission: MissionInfo(**slot_req_table[mission]) for mission in slot_req_table + } + + self.build_location_to_mission_mapping() # Look for and set SC2PATH. # check_game_install_path() returns True if and only if it finds + sets SC2PATH. if "SC2PATH" not in os.environ and check_game_install_path(): check_mod_install() - if cmd in {"PrintJSON"}: - if "receiving" in args: - if self.slot_concerns_self(args["receiving"]): - self.announcements.append(args["data"]) - return - if "item" in args: - if self.slot_concerns_self(args["item"].player): - self.announcements.append(args["data"]) + def on_print_json(self, args: dict): + if "receiving" in args and self.slot_concerns_self(args["receiving"]): + relevant = True + elif "item" in args and self.slot_concerns_self(args["item"].player): + relevant = True + else: + relevant = False + + if relevant: + self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"]))) + + super(SC2Context, self).on_print_json(args) def run_gui(self): - from kvui import GameManager, HoverBehavior, ServerToolTip, fade_in_animation + from kvui import GameManager, HoverBehavior, ServerToolTip from kivy.app import App from kivy.clock import Clock from kivy.uix.tabbedpanel import TabbedPanelItem @@ -190,6 +195,7 @@ class SC2Context(CommonContext): class MissionButton(HoverableButton): tooltip_text = StringProperty("Test") + ctx: SC2Context def __init__(self, *args, **kwargs): super(HoverableButton, self).__init__(*args, **kwargs) @@ -210,10 +216,7 @@ class SC2Context(CommonContext): self.ctx.current_tooltip = self.layout def on_leave(self): - if self.ctx.current_tooltip: - App.get_running_app().root.remove_widget(self.ctx.current_tooltip) - - self.ctx.current_tooltip = None + self.ctx.ui.clear_tooltip() @property def ctx(self) -> CommonContext: @@ -235,13 +238,20 @@ class SC2Context(CommonContext): mission_panel = None last_checked_locations = {} mission_id_to_button = {} - launching = False + launching: typing.Union[bool, int] = False # if int -> mission ID refresh_from_launching = True first_check = True + ctx: SC2Context def __init__(self, ctx): super().__init__(ctx) + def clear_tooltip(self): + if self.ctx.current_tooltip: + App.get_running_app().root.remove_widget(self.ctx.current_tooltip) + + self.ctx.current_tooltip = None + def build(self): container = super().build() @@ -256,7 +266,7 @@ class SC2Context(CommonContext): def build_mission_table(self, dt): if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or - not self.refresh_from_launching)) or self.first_check: + not self.refresh_from_launching)) or self.first_check: self.refresh_from_launching = True self.mission_panel.clear_widgets() @@ -267,12 +277,7 @@ class SC2Context(CommonContext): self.mission_id_to_button = {} categories = {} - available_missions = [] - unfinished_locations = initialize_blank_mission_dict(self.ctx.mission_req_table) - unfinished_missions = calc_unfinished_missions(self.ctx.checked_locations, - self.ctx.mission_req_table, - self.ctx, available_missions=available_missions, - unfinished_locations=unfinished_locations) + available_missions, unfinished_missions = calc_unfinished_missions(self.ctx) # separate missions into categories for mission in self.ctx.mission_req_table: @@ -283,7 +288,8 @@ class SC2Context(CommonContext): for category in categories: category_panel = MissionCategory() - category_panel.add_widget(Label(text=category, size_hint_y=None, height=50, outline_width=1)) + category_panel.add_widget( + Label(text=category, size_hint_y=None, height=50, outline_width=1)) # Map is completed for mission in categories[category]: @@ -295,7 +301,9 @@ class SC2Context(CommonContext): text = f"[color=6495ED]{text}[/color]" tooltip = f"Uncollected locations:\n" - tooltip += "\n".join(location for location in unfinished_locations[mission]) + tooltip += "\n".join([self.ctx.location_names[loc] for loc in + self.ctx.locations_for_mission(mission) + if loc in self.ctx.missing_locations]) elif mission in available_missions: text = f"[color=FFFFFF]{text}[/color]" # Map requirements not met @@ -303,7 +311,7 @@ class SC2Context(CommonContext): text = f"[color=a9a9a9]{text}[/color]" tooltip = f"Requires: " if len(self.ctx.mission_req_table[mission].required_world) > 0: - tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission-1] for + tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for req_mission in self.ctx.mission_req_table[mission].required_world) @@ -325,13 +333,17 @@ class SC2Context(CommonContext): self.refresh_from_launching = False self.mission_panel.clear_widgets() - self.mission_panel.add_widget(Label(text="Launching Mission")) + self.mission_panel.add_widget(Label(text="Launching Mission: " + + lookup_id_to_mission[self.launching])) + if self.ctx.ui: + self.ctx.ui.clear_tooltip() def mission_callback(self, button): if not self.launching: - self.ctx.play_mission(list(self.mission_id_to_button.keys()) - [list(self.mission_id_to_button.values()).index(button)]) - self.launching = True + mission_id: int = list(self.mission_id_to_button.values()).index(button) + self.ctx.play_mission(list(self.mission_id_to_button) + [mission_id]) + self.launching = mission_id Clock.schedule_once(self.finish_launching, 10) def finish_launching(self, dt): @@ -349,7 +361,7 @@ class SC2Context(CommonContext): def play_mission(self, mission_id): if self.missions_unlocked or \ - is_mission_available(mission_id, self.checked_locations, self.mission_req_table): + is_mission_available(self, mission_id): if self.sc2_run_task: if not self.sc2_run_task.done(): sc2_logger.warning("Starcraft 2 Client is still running!") @@ -358,12 +370,29 @@ class SC2Context(CommonContext): sc2_logger.warning("Launching Mission without Archipelago authentication, " "checks will not be registered to server.") self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id), - name="Starcraft 2 Launch") + name="Starcraft 2 Launch") else: sc2_logger.info( f"{lookup_id_to_mission[mission_id]} is not currently unlocked. " f"Use /unfinished or /available to see what is available.") + def build_location_to_mission_mapping(self): + mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = { + mission_info.id: set() for mission_info in self.mission_req_table.values() + } + + for loc in self.server_locations: + mission_id, objective = divmod(loc - SC2WOL_LOC_ID_OFFSET, victory_modulo) + mission_id_to_location_ids[mission_id].add(objective) + self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in + mission_id_to_location_ids.items()} + + def locations_for_mission(self, mission: str): + mission_id: int = self.mission_req_table[mission].id + objectives = self.mission_id_to_location_ids[self.mission_req_table[mission].id] + for objective in objectives: + yield SC2WOL_LOC_ID_OFFSET + mission_id * 100 + objective + async def main(): multiprocessing.freeze_support() @@ -459,11 +488,7 @@ def calc_difficulty(difficulty): return 'X' -async def starcraft_launch(ctx: SC2Context, mission_id): - ctx.rec_announce_pos = len(ctx.items_rec_to_announce) - ctx.sent_announce_pos = len(ctx.items_sent_to_announce) - ctx.announcements_pos = len(ctx.announcements) - +async def starcraft_launch(ctx: SC2Context, mission_id: int): sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.") with DllDirectory(None): @@ -472,32 +497,29 @@ async def starcraft_launch(ctx: SC2Context, mission_id): class ArchipelagoBot(sc2.bot_ai.BotAI): - game_running = False - mission_completed = False - first_bonus = False - second_bonus = False - third_bonus = False - fourth_bonus = False - fifth_bonus = False - sixth_bonus = False - seventh_bonus = False - eight_bonus = False - ctx: SC2Context = None - mission_id = 0 + game_running: bool = False + mission_completed: bool = False + boni: typing.List[bool] + setup_done: bool + ctx: SC2Context + mission_id: int can_read_game = False - last_received_update = 0 + last_received_update: int = 0 def __init__(self, ctx: SC2Context, mission_id): + self.setup_done = False self.ctx = ctx self.mission_id = mission_id + self.boni = [False for _ in range(max_bonus)] super(ArchipelagoBot, self).__init__() async def on_step(self, iteration: int): game_state = 0 - if iteration == 0: + if not self.setup_done: + self.setup_done = True start_items = calculate_items(self.ctx.items_received) if self.ctx.difficulty_override >= 0: difficulty = calc_difficulty(self.ctx.difficulty_override) @@ -511,36 +533,10 @@ class ArchipelagoBot(sc2.bot_ai.BotAI): self.last_received_update = len(self.ctx.items_received) else: - if self.ctx.announcement_pos < len(self.ctx.announcements): - index = 0 - message = "" - while index < len(self.ctx.announcements[self.ctx.announcement_pos]): - message += self.ctx.announcements[self.ctx.announcement_pos][index]["text"] - index += 1 - - index = 0 - start_rem_pos = -1 - # Remove unneeded [Color] tags - while index < len(message): - if message[index] == '[': - start_rem_pos = index - index += 1 - elif message[index] == ']' and start_rem_pos > -1: - temp_msg = "" - - if start_rem_pos > 0: - temp_msg = message[:start_rem_pos] - if index < len(message) - 1: - temp_msg += message[index + 1:] - - message = temp_msg - index += start_rem_pos - index - start_rem_pos = -1 - else: - index += 1 - + if not self.ctx.announcements.empty(): + message = self.ctx.announcements.get(timeout=1) await self.chat_send("SendMessage " + message) - self.ctx.announcement_pos += 1 + self.ctx.announcements.task_done() # Archipelago reads the health for unit in self.all_own_units(): @@ -568,169 +564,97 @@ class ArchipelagoBot(sc2.bot_ai.BotAI): if game_state & (1 << 1) and not self.mission_completed: if self.mission_id != 29: print("Mission Completed") - await self.ctx.send_msgs([ - {"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id]}]) + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', + "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id]}]) self.mission_completed = True else: print("Game Complete") await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}]) self.mission_completed = True - if game_state & (1 << 2) and not self.first_bonus: - print("1st Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 1]}]) - self.first_bonus = True - - if not self.second_bonus and game_state & (1 << 3): - print("2nd Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 2]}]) - self.second_bonus = True - - if not self.third_bonus and game_state & (1 << 4): - print("3rd Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 3]}]) - self.third_bonus = True - - if not self.fourth_bonus and game_state & (1 << 5): - print("4th Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 4]}]) - self.fourth_bonus = True - - if not self.fifth_bonus and game_state & (1 << 6): - print("5th Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 5]}]) - self.fifth_bonus = True - - if not self.sixth_bonus and game_state & (1 << 7): - print("6th Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 6]}]) - self.sixth_bonus = True - - if not self.seventh_bonus and game_state & (1 << 8): - print("6th Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 7]}]) - self.seventh_bonus = True - - if not self.eight_bonus and game_state & (1 << 9): - print("6th Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 8]}]) - self.eight_bonus = True + for x, completed in enumerate(self.boni): + if not completed and game_state & (1 << (x + 2)): + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', + "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id + x + 1]}]) + self.boni[x] = True else: await self.chat_send("LostConnection - Lost connection to game.") -def calc_objectives_completed(mission, missions_info, locations_done, unfinished_locations, ctx): - objectives_complete = 0 - - if missions_info[mission].extra_locations > 0: - for i in range(missions_info[mission].extra_locations): - if (missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i) in locations_done: - objectives_complete += 1 - else: - unfinished_locations[mission].append(ctx.location_names[ - missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i]) - - return objectives_complete - - else: - return -1 - - -def request_unfinished_missions(locations_done, location_table, ui, ctx): - if location_table: +def request_unfinished_missions(ctx: SC2Context): + if ctx.mission_req_table: message = "Unfinished Missions: " - unlocks = initialize_blank_mission_dict(location_table) - unfinished_locations = initialize_blank_mission_dict(location_table) + unlocks = initialize_blank_mission_dict(ctx.mission_req_table) + unfinished_locations = initialize_blank_mission_dict(ctx.mission_req_table) - unfinished_missions = calc_unfinished_missions(locations_done, location_table, ctx, unlocks=unlocks, - unfinished_locations=unfinished_locations) + _, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks) - message += ", ".join(f"{mark_up_mission_name(mission, location_table, ui,unlocks)}[{location_table[mission].id}] " + + message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[mission].id}] " + mark_up_objectives( - f"[{unfinished_missions[mission]}/{location_table[mission].extra_locations}]", + f"[{len(unfinished_missions[mission])}/" + f"{sum(1 for _ in ctx.locations_for_mission(mission))}]", ctx, unfinished_locations, mission) for mission in unfinished_missions) - if ui: - ui.log_panels['All'].on_message_markup(message) - ui.log_panels['Starcraft2'].on_message_markup(message) + if ctx.ui: + ctx.ui.log_panels['All'].on_message_markup(message) + ctx.ui.log_panels['Starcraft2'].on_message_markup(message) else: sc2_logger.info(message) else: sc2_logger.warning("No mission table found, you are likely not connected to a server.") -def calc_unfinished_missions(locations_done, locations, ctx, unlocks=None, unfinished_locations=None, - available_missions=[]): +def calc_unfinished_missions(ctx: SC2Context, unlocks=None): unfinished_missions = [] locations_completed = [] if not unlocks: - unlocks = initialize_blank_mission_dict(locations) + unlocks = initialize_blank_mission_dict(ctx.mission_req_table) - if not unfinished_locations: - unfinished_locations = initialize_blank_mission_dict(locations) - - if len(available_missions) > 0: - available_missions = [] - - available_missions.extend(calc_available_missions(locations_done, locations, unlocks)) + available_missions = calc_available_missions(ctx, unlocks) for name in available_missions: - if not locations[name].extra_locations == -1: - objectives_completed = calc_objectives_completed(name, locations, locations_done, unfinished_locations, ctx) - - if objectives_completed < locations[name].extra_locations: + objectives = set(ctx.locations_for_mission(name)) + if objectives: + objectives_completed = ctx.checked_locations & objectives + if len(objectives_completed) < len(objectives): unfinished_missions.append(name) locations_completed.append(objectives_completed) - else: + else: # infer that this is the final mission as it has no objectives unfinished_missions.append(name) locations_completed.append(-1) - return {unfinished_missions[i]: locations_completed[i] for i in range(len(unfinished_missions))} + return available_missions, dict(zip(unfinished_missions, locations_completed)) -def is_mission_available(mission_id_to_check, locations_done, locations): - unfinished_missions = calc_available_missions(locations_done, locations) +def is_mission_available(ctx: SC2Context, mission_id_to_check): + unfinished_missions = calc_available_missions(ctx) - return any(mission_id_to_check == locations[mission].id for mission in unfinished_missions) + return any(mission_id_to_check == ctx.mission_req_table[mission].id for mission in unfinished_missions) -def mark_up_mission_name(mission, location_table, ui, unlock_table): +def mark_up_mission_name(ctx: SC2Context, mission, unlock_table): """Checks if the mission is required for game completion and adds '*' to the name to mark that.""" - if location_table[mission].completion_critical: - if ui: + if ctx.mission_req_table[mission].completion_critical: + if ctx.ui: message = "[color=AF99EF]" + mission + "[/color]" else: message = "*" + mission + "*" else: message = mission - if ui: + if ctx.ui: unlocks = unlock_table[mission] if len(unlocks) > 0: - pre_message = f"[ref={list(location_table).index(mission)}|Unlocks: " - pre_message += ", ".join(f"{unlock}({location_table[unlock].id})" for unlock in unlocks) + pre_message = f"[ref={list(ctx.mission_req_table).index(mission)}|Unlocks: " + pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[unlock].id})" for unlock in unlocks) pre_message += f"]" message = pre_message + message + "[/ref]" @@ -743,7 +667,7 @@ def mark_up_objectives(message, ctx, unfinished_locations, mission): if ctx.ui: locations = unfinished_locations[mission] - pre_message = f"[ref={list(ctx.mission_req_table).index(mission)+30}|" + pre_message = f"[ref={list(ctx.mission_req_table).index(mission) + 30}|" pre_message += "
".join(location for location in locations) pre_message += f"]" formatted_message = pre_message + message + "[/ref]" @@ -751,90 +675,91 @@ def mark_up_objectives(message, ctx, unfinished_locations, mission): return formatted_message -def request_available_missions(locations_done, location_table, ui): - if location_table: +def request_available_missions(ctx: SC2Context): + if ctx.mission_req_table: message = "Available Missions: " # Initialize mission unlock table - unlocks = initialize_blank_mission_dict(location_table) + unlocks = initialize_blank_mission_dict(ctx.mission_req_table) - missions = calc_available_missions(locations_done, location_table, unlocks) + missions = calc_available_missions(ctx, unlocks) message += \ - ", ".join(f"{mark_up_mission_name(mission, location_table, ui, unlocks)}[{location_table[mission].id}]" + ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}" + f"[{ctx.mission_req_table[mission].id}]" for mission in missions) - if ui: - ui.log_panels['All'].on_message_markup(message) - ui.log_panels['Starcraft2'].on_message_markup(message) + if ctx.ui: + ctx.ui.log_panels['All'].on_message_markup(message) + ctx.ui.log_panels['Starcraft2'].on_message_markup(message) else: sc2_logger.info(message) else: sc2_logger.warning("No mission table found, you are likely not connected to a server.") -def calc_available_missions(locations_done, locations, unlocks=None): +def calc_available_missions(ctx: SC2Context, unlocks=None): available_missions = [] missions_complete = 0 # Get number of missions completed - for loc in locations_done: - if loc % 100 == 0: + for loc in ctx.checked_locations: + if loc % victory_modulo == 0: missions_complete += 1 - for name in locations: + for name in ctx.mission_req_table: # Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips if unlocks: - for unlock in locations[name].required_world: - unlocks[list(locations)[unlock-1]].append(name) + for unlock in ctx.mission_req_table[name].required_world: + unlocks[list(ctx.mission_req_table)[unlock - 1]].append(name) - if mission_reqs_completed(name, missions_complete, locations_done, locations): + if mission_reqs_completed(ctx, name, missions_complete): available_missions.append(name) return available_missions -def mission_reqs_completed(location_to_check, missions_complete, locations_done, locations): +def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete): """Returns a bool signifying if the mission has all requirements complete and can be done - Keyword arguments: + Arguments: + ctx -- instance of SC2Context locations_to_check -- the mission string name to check missions_complete -- an int of how many missions have been completed - locations_done -- a list of the location ids that have been complete - locations -- a dict of MissionInfo for mission requirements for this world""" - if len(locations[location_to_check].required_world) >= 1: +""" + if len(ctx.mission_req_table[mission_name].required_world) >= 1: # A check for when the requirements are being or'd or_success = False # Loop through required missions - for req_mission in locations[location_to_check].required_world: + for req_mission in ctx.mission_req_table[mission_name].required_world: req_success = True # Check if required mission has been completed - if not (locations[list(locations)[req_mission-1]].id * 100 + SC2WOL_LOC_ID_OFFSET) in locations_done: - if not locations[location_to_check].or_requirements: + if not (ctx.mission_req_table[list(ctx.mission_req_table)[req_mission - 1]].id * + victory_modulo + SC2WOL_LOC_ID_OFFSET) in ctx.checked_locations: + if not ctx.mission_req_table[mission_name].or_requirements: return False else: req_success = False # Recursively check required mission to see if it's requirements are met, in case !collect has been done - if not mission_reqs_completed(list(locations)[req_mission-1], missions_complete, locations_done, - locations): - if not locations[location_to_check].or_requirements: + if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete): + if not ctx.mission_req_table[mission_name].or_requirements: return False else: req_success = False # If requirement check succeeded mark or as satisfied - if locations[location_to_check].or_requirements and req_success: + if ctx.mission_req_table[mission_name].or_requirements and req_success: or_success = True - if locations[location_to_check].or_requirements: + if ctx.mission_req_table[mission_name].or_requirements: # Return false if or requirements not met if not or_success: return False # Check number of missions - if missions_complete >= locations[location_to_check].number: + if missions_complete >= ctx.mission_req_table[mission_name].number: return True else: return False @@ -929,7 +854,7 @@ class DllDirectory: self.set(self._old) @staticmethod - def get() -> str: + def get() -> typing.Optional[str]: if sys.platform == "win32": n = ctypes.windll.kernel32.GetDllDirectoryW(0, None) buf = ctypes.create_unicode_buffer(n) diff --git a/Utils.py b/Utils.py index 4b2300a870..c362131d75 100644 --- a/Utils.py +++ b/Utils.py @@ -35,7 +35,7 @@ class Version(typing.NamedTuple): build: int -__version__ = "0.3.4" +__version__ = "0.3.5" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") diff --git a/worlds/sc2wol/MissionTables.py b/worlds/sc2wol/MissionTables.py index ecd1da4922..4f1b1157ec 100644 --- a/worlds/sc2wol/MissionTables.py +++ b/worlds/sc2wol/MissionTables.py @@ -69,8 +69,8 @@ vanilla_mission_req_table = { "Zero Hour": MissionInfo(3, 4, [2], "Mar Sara", completion_critical=True), "Evacuation": MissionInfo(4, 4, [3], "Colonist"), "Outbreak": MissionInfo(5, 3, [4], "Colonist"), - "Safe Haven": MissionInfo(6, 1, [5], "Colonist", number=7), - "Haven's Fall": MissionInfo(7, 1, [5], "Colonist", number=7), + "Safe Haven": MissionInfo(6, 4, [5], "Colonist", number=7), + "Haven's Fall": MissionInfo(7, 4, [5], "Colonist", number=7), "Smash and Grab": MissionInfo(8, 5, [3], "Artifact", completion_critical=True), "The Dig": MissionInfo(9, 4, [8], "Artifact", number=8, completion_critical=True), "The Moebius Factor": MissionInfo(10, 9, [9], "Artifact", number=11, completion_critical=True), diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index cf3175bd6e..4f9b33609f 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -43,6 +43,7 @@ class SC2WoLWorld(World): locked_locations: typing.List[str] location_cache: typing.List[Location] mission_req_table = {} + required_client_version = 0, 3, 5 def __init__(self, world: MultiWorld, player: int): super(SC2WoLWorld, self).__init__(world, player) From 0444fdc379aab5d41e4052686e55b896f96f3de6 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Wed, 31 Aug 2022 20:20:30 -0400 Subject: [PATCH 18/24] SM: wasteland ap (#983) --- .../variaRandomizer/graph/vanilla/graph_access.py | 13 ++++++++++++- .../graph/vanilla/graph_locations.py | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py index eebff84c52..b74b69026e 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py @@ -294,7 +294,18 @@ accessPoints = [ sm.canGetBackFromRidleyZone(), sm.canPassWastelandDessgeegas(), sm.canPassRedKiHunters())), - 'RidleyRoomOut': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main'])) + 'RidleyRoomOut': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main'])), + 'Wasteland': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']), + sm.canGetBackFromRidleyZone(), + sm.canPassWastelandDessgeegas())) + }, internal=True), + AccessPoint('Wasteland', 'LowerNorfair', { + # no transition to firefleas to exlude pb of shame location when starting at firefleas top + 'Ridley Zone': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']), + sm.traverse('WastelandLeft'), + sm.canGetBackFromRidleyZone(), + sm.canPassWastelandDessgeegas(), + sm.canPassNinjaPirates())) }, internal=True), AccessPoint('Three Muskateers Room Left', 'LowerNorfair', { 'Firefleas': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']), diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py index b8a1d3f44e..671368e831 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py @@ -797,10 +797,10 @@ locationsDict["Power Bomb (lower Norfair above fire flea room)"].Available = ( lambda sm: SMBool(True) ) locationsDict["Power Bomb (Power Bombs of shame)"].AccessFrom = { - 'Ridley Zone': lambda sm: sm.canUsePowerBombs() + 'Wasteland': lambda sm: sm.canUsePowerBombs() } locationsDict["Power Bomb (Power Bombs of shame)"].Available = ( - lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']) + lambda sm: SMBool(True) ) locationsDict["Missile (lower Norfair near Wave Beam)"].AccessFrom = { 'Firefleas': lambda sm: SMBool(True) From b115bdafe78e34261b8fd8c83fdde54db469e50d Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 1 Sep 2022 09:30:28 +0200 Subject: [PATCH 19/24] CI/Doc: Use pytest subtests (#986) * CI/Doc: use pytest-subtests * CI: clean up pip installs a bit * make lint and unittests install the same stuff * make sure to install wheel, which is a recommended (not required) dependency for everything pip --- .github/workflows/lint.yml | 4 ++-- .github/workflows/unittests.yml | 4 ++-- docs/running from source.md | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d7cc3c7439..28adb50026 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,8 +18,8 @@ jobs: python-version: 3.9 - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install flake8 pytest + python -m pip install --upgrade pip wheel + pip install flake8 pytest pytest-subtests if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 1c8ab10c70..4d0ceaec87 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -32,8 +32,8 @@ jobs: python-version: ${{ matrix.python.version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install flake8 pytest + python -m pip install --upgrade pip wheel + pip install flake8 pytest pytest-subtests python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt" - name: Unittests run: | diff --git a/docs/running from source.md b/docs/running from source.md index 4360b28c16..39addd0a28 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -56,3 +56,8 @@ SNI is required to use SNIClient. If not integrated into the project, it has to You can get the latest SNI release at [SNI Github releases](https://github.com/alttpo/sni/releases). It should be dropped as "SNI" into the root folder of the project. Alternatively, you can point the sni setting in host.yaml at your SNI folder. + + +## Running tests + +Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder. From 03f66a922d42f492ff751fede981d94e14dd4ee4 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Thu, 1 Sep 2022 21:21:53 +0200 Subject: [PATCH 20/24] sm64ex: Fix a Location (#979) --- worlds/sm64ex/Locations.py | 4 ++-- worlds/sm64ex/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/sm64ex/Locations.py b/worlds/sm64ex/Locations.py index 1995abf425..9a3db29c26 100644 --- a/worlds/sm64ex/Locations.py +++ b/worlds/sm64ex/Locations.py @@ -245,7 +245,7 @@ locBitFS_table = { locWMotR_table = { "Wing Mario Over the Rainbow Red Coins": 3626154, - "Wing Mario Over the Rainbow 1Up Block": 3626242 + "Wing Mario Over the Rainbow 1Up Block": 3626243 } locBitS_table = { @@ -268,4 +268,4 @@ location_table = {**locBoB_table,**locWhomp_table,**locJRB_table,**locCCM_table, **locWDW_table,**locTTM_table,**locTHI_table,**locTTC_table,**locRR_table, \ **loc100Coin_table,**locPSS_table,**locSA_table,**locBitDW_table,**locTotWC_table, \ **locCotMC_table, **locVCutM_table, **locBitFS_table, **locWMotR_table, **locBitS_table, \ - **locSS_table} \ No newline at end of file + **locSS_table} diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 447a09d431..cf8a8e875d 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -34,7 +34,7 @@ class SM64World(World): item_name_to_id = item_table location_name_to_id = location_table - data_version = 7 + data_version = 8 required_client_version = (0, 3, 0) area_connections: typing.Dict[int, int] From e413619c26c30322110f963e36fef044b64073b2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 1 Sep 2022 21:25:06 +0200 Subject: [PATCH 21/24] Tests: verify that a world doesn't use the same ID multiple times (#985) --- test/general/TestIDs.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/general/TestIDs.py b/test/general/TestIDs.py index f91775c8de..db1c9461b9 100644 --- a/test/general/TestIDs.py +++ b/test/general/TestIDs.py @@ -52,3 +52,13 @@ class TestIDs(unittest.TestCase): else: for location_id in world_type.location_id_to_name: self.assertGreater(location_id, 0) + + def testDuplicateItemIDs(self): + for gamename, world_type in AutoWorldRegister.world_types.items(): + with self.subTest(game=gamename): + self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id)) + + def testDuplicateLocationIDs(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)) From 8d2333006a43407e2fa7fb5ad88fc00b08ef124a Mon Sep 17 00:00:00 2001 From: skrawpie <21212370+skrawpie@users.noreply.github.com> Date: Thu, 1 Sep 2022 15:26:04 -0400 Subject: [PATCH 22/24] Minecraft: Added shuffled recipe list to en_Minecraft.md (#980) Co-authored-by: KonoTyran --- worlds/minecraft/docs/en_Minecraft.md | 87 ++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/worlds/minecraft/docs/en_Minecraft.md b/worlds/minecraft/docs/en_Minecraft.md index b67107a8fc..2d4f063b79 100644 --- a/worlds/minecraft/docs/en_Minecraft.md +++ b/worlds/minecraft/docs/en_Minecraft.md @@ -7,9 +7,9 @@ config file. ## What does randomization do to this game? -Recipes are removed from the crafting book and shuffled into the item pool. It can also optionally change which +Some recipes are locked from being able to be crafted and shuffled into the item pool. It can also optionally change which structures appear in each dimension. Crafting recipes are re-learned when they are received from other players as item -checks, and occasionally when completing your own achievements. +checks, and occasionally when completing your own achievements. See below for which recipes are shuffled. ## What is considered a location check in minecraft? @@ -25,3 +25,86 @@ inventory directly. Victory is achieved when the player kills the Ender Dragon, enters the portal in The End, and completes the credits sequence either by skipping it or watching hit play out. + +## Which recipes are locked? + +* Archery + * Bow + * Arrow + * Crossbow +* Brewing + * Blaze Powder + * Brewing Stand +* Enchanting + * Enchanting Table + * Bookshelf +* Bucket +* Flint & Steel +* All Beds +* Bottles +* Shield +* Fishing Rod + * Fishing Rod + * Carrot on a Stick + * Warped Fungus on a Stick +* Campfire + * Campfire + * Soul Campfire +* Spyglass +* Lead +* Progressive Weapons + * Tier I + * Stone Sword + * Stone Axe + * Tier II + * Iron Sword + * Iron Axe + * Tier III + * Diamond Sword + * Diamond Axe +* Progessive Tools + * Tier I + * Stone Shovel + * Stone Hoe + * Tier II + * Iron Shovel + * Iron Hoe + * Tier III + * Diamond Shovel + * Diamond Hoe + * Netherite Ingot +* Progressive Armor + * Tier I + * Iron Helmet + * Iron Chestplate + * Iron Leggings + * Iron Boots + * Tier II + * Diamond Helmet + * Diamond Chestplate + * Diamond Leggings + * Diamond Boots +* Progressive Resource Crafting + * Tier I + * Iron Ingot from Nuggets + * Iron Nugget + * Gold Ingot from Nuggets + * Gold Nugget + * Furnace + * Blast Furnace + * Tier II + * Redstone + * Redstone Block + * Glowstone + * Iron Ingot from Iron Block + * Iron Block + * Gold Ingot from Gold Block + * Gold Block + * Diamond + * Diamond Block + * Netherite Block + * Netherite Ingot from Netherite Block + * Anvil + * Emerald + * Emerald Block + * Copper Block From b14d694e1e22cdd901d317837bc2bf533fa2e444 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 1 Sep 2022 13:45:14 -0500 Subject: [PATCH 23/24] templates: fix bug report label --- .github/ISSUE_TEMPLATE/bug_report.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index dff9a56651..d4c8702da0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -2,7 +2,7 @@ name: Bug Report description: File a bug report. title: "Bug: " labels: - - bug + - bug / fix body: - type: markdown attributes: @@ -32,4 +32,4 @@ body: - Local generation - While playing validations: - required: true \ No newline at end of file + required: true From f7d107fc0c89a4d34f6ae6a4c4b563a14b50c650 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 2 Sep 2022 03:35:41 +0200 Subject: [PATCH 24/24] Subnautica: add some more missed aggressive creatures --- worlds/subnautica/Creatures.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/subnautica/Creatures.py b/worlds/subnautica/Creatures.py index 687c3732a9..cb34f261f5 100644 --- a/worlds/subnautica/Creatures.py +++ b/worlds/subnautica/Creatures.py @@ -55,7 +55,6 @@ all_creatures: Dict[str, int] = { "Sea Emperor Juvenile": 1700, } -# be nice and make these require Stasis Rifle aggressive: Set[str] = { "Cave Crawler", # is very easy without Stasis Rifle, but included for consistency "Crashfish", @@ -75,6 +74,8 @@ aggressive: Set[str] = { "Lava Lizard", "Sea Dragon Leviathan", "River Prowler", + "Ghost Leviathan Juvenile", + "Ghost Leviathan" } containment: Set[str] = { # creatures that have to be raised from eggs