Compare commits

..

769 Commits

Author SHA1 Message Date
Fabian Dill
1db6b67953 Tests: load custom tests from apworld 2023-07-01 02:41:51 +02:00
Trevor L
fa3c132304 Hylics 2: Add missing location (#1917)
* Hylics 2: Add missing location

* Hylics 2: Change data_version
2023-06-30 17:46:32 -05:00
digiholic
6226713c4d MMBN3: Press program now has proper color index when received remotely (#1918) 2023-06-30 17:34:53 -05:00
Justus Lind
b56da79890 Muse Dash: Add 2023 Anniversary songs and remove a hidden song (#1916)
* Remove CHAOS Glitch. Add test to check for removed songs.

* Add to game list

* Fix oversight with 0 difficulty songs. Fix naming of test.

* Add new songs and update other data.

* Fix accidental copy paste
2023-06-30 08:10:58 -05:00
PoryGone
1d6345d3a2 SA2: Add troubleshooting note about Skip Intro and Cutscene Traps (#1915) 2023-06-29 22:28:08 -05:00
el-u
51a639ceaf lufia2ac: use an appropriate dungeon sprite and battle theme for each boss (#1914) 2023-06-29 22:21:46 -05:00
digiholic
7ecb1e6d6c Docs: Adds MMBN3 to Readme.md (#1912) 2023-06-29 15:01:37 -05:00
Zach Parks
c9fb443c64 OriBF: Move Ori and the Blind Forest to worlds_disabled. (#1906)
* OriBF: Move Ori and the Blind Forest to `worlds_disabled/`

* Add readme for `worlds_disabled` folder

* fix link

* fix link 2

* Remove useless comment

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>

---------

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2023-06-29 13:36:48 -05:00
digiholic
325299286b Mega Man Battle Network 3: Implement New Game (#1198)
* Initializes MMBN3 world with empty files

* Adds item names to item dict

* Adds locations and names

* Adds skeleton of MMBN3Client. Mostly copy pasta from OOT

* Fixed some style and formatting

* More incremental Lua tests

* Adds all locations and checking to Lua connector

* Made class definitions for TextPet Parser

* Begun connecting item delivery system through lua and textpet

* Lua Connection can now send test items

* Item Delivery is now parameterized. Test command can send any chip

* Adds the ability to send non-chip items

* Fixes name errors in python client

* Fixes count for zenny, attempts to fix bugfrags

* Fixes an issue where you always received 255 bugfrags

* Converts zenny and bugfrag amounts to little endian bytecode

* Checks game state before sending chips

Adds debug option to display information overlayed on rom
Fixes chip indexing issue for chips with ids over 255
Minor text fixes

* Adds in some animation reset instructions during item get message

* Stores previously collected item index in save, re-sends missing items

* Adds title screen check before sending locations

Loading items from save could not be done via RAM. Had to be added in
assembly

* Adds progressive undernet check

* Added library for lzss decoding bits of rom

* More progress on parsing text events from ROM

* Adds a way to inject messages into ScriptArchive data structure and generate bytecode

* Adds Item definitions, passes to client

* Adds regions and item collection rules

* Touched up a few names and values that have changed in preparation for the final patching

* Modifying messages via item is now successful

* Added generate_output hook to generate ROM data

* Generates ROM successfully

* Fixes navi cust give index

* Whoops forgot to wrap this in brackets

* Injects extra scripts for undernet rankings

* Programs had ammount and color swapped

* Prompts the user for their username when connecting

* Adds flagClear to the list of commands to avoid overwriting

* Fixes message box crashes and several other multiworld issues

* Fixes IDs and names of several items and locations

* Added .gba to gitignore

* Fixes compatibility after recent rebase

* Fixes some locations and items that are otherwise unobtainable

* Attempts to make a working launcher in the installer

* Creates installer and fixes several inaccessible locations

* Many minor changes to items, locations, and requirements made during testing

* Adds an info page for MMBN3

* Fixes failing tests by removing duplicate IDs and properly marking progression items

* Accidentally forgot to un-remove the thing

* Whoops, changed this by accident

* Updates self.world references to self.multiworld

* Fixes imports to use from imports instead of using the namespace

* Removed some leftover merge artifacts from inno setup

* Puts back that darned signtool line again

* Adds Overworld Metro keys as items

* Adds TamaCode and puts shortcuts behind cyber passes

* Fixes Numberman code 16 check

* Fixes metro access logic and adds text to metro

* Reworks Lua to fix crashing when many items are queued

* Items for other BN3 games for different players are no longer given in the main player's ROM as well

* Fixes incorrect Item ID for ACDC Metro

* Fixes multi-box text messages

* Adds timer before sending an item

* Forgot to remove the second box of SubMems

* Updates patch and lua to prevent softlocks and crashes

* Adds options for extra undernet ranks, exclude jobs

* Extra GigFreez now gives 20 bugfrags

* Additional Progressive Undernets can no longer appear on the WWW Base

* Moves item signal byte to empty area of flags instead of end of RAM

* Adds Chocolate Shop locations and navi chips to fill them

* Fixes save crash, and added chocolates to lua

* Fixes chocolate stand selling out text, removes DrillMan cube in Undernet

* Replaces old messaging system with direct memory manipulation for receiving items

* Removes NDSPY requirements from MMBN3 by manually adapting the GBA's lz10 algorithm

* Fixes the names of Hospital-1 Locations

* Adds Canary Bit to avoid sending checks when title screen check fails

* Gaining a cybermetro pass will now open the shortcut immediately

* Randomizes the two accessible areas of Undernet 7, adds Hammer as item

* Adds new locations to connector lua

* Injects the name of the item into trade quests

* Fixes copy-paste error in docs

* Fixes merge artifacts and depracated code

* Nut-wafer stand now faces Lan the right way after buying

* Removes unused Goal Option and updates the readme to include most recent changes

* Touch-ups and formatting changes

* The Great Fillerization update. Dozens of items changed to Filler

* Replaces instances of Mega Man with MegaMan

* Update worlds/mmbn3/docs/en_MegaMan Battle Network 3.md

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>

* Update worlds/mmbn3/__init__.py

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* Changes code ordering to suit base class's

* assert_generate now checks for roms. Minor text fixes

* Makes player specific frequency and excluded location options

* Apply suggestions from code review

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>

* Addresses suggested changes from PR review

* Replaces ndspy lz10 with MIT-compliant nlzss lz10

* apworld compatibility fix for mmbn3_options from utils

* Addressing more comments by el-u

* APworld will now pull patch from zip folder

* Apply suggestions from code review

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>

* Cleaned up comments for progressive undernet ROM function, moved index list to field to avoid re-initializing

* Removes improper player-indexed location/item dicts, replaces with world member variables

* Avoids redefining list in progressive undernet ROM function

* Filler items can no longer be generated beyond their specified amounts

* Fixes list copying issue with item frequencies

* Adds BN3 Client Generation back into Launcher settings

* Fixes typos causing huge problems

* Fixed non-relative import for apworld

* Removes custom enum implementation that broke pickle

* Displays message when attempting to load an incorrect ROM, will not attempt to patch it

* Filler items can now only be placed once

* Changes path in setup doc to match Lua path changes

* Fixes file extension for MMBN3 file

* Replaces magic number with reference to value in NetUtils

* Moves victory rules to set_rules. Removes commented out code

* Rewrites Lua script to send block of memory

* Fixes off-by-one error in sending bytes for locations

* Fixes issue with invalid characters in text parsing, and WWW monitor text box parsing

* Moves trade text injection to init so it has access to options

* Attempts to split the text boxes for hinted items

* Trade checks now provide hints if the option is set for them

* Fixes escape character issue for BizHawk 2.9.1

Something in Bizhawk lua parsing changed to dislike the escaped tilde.
I'm not even entirely sure why it was escaped in the first place, but
this should fix the compatibility of it.

* Re-adds desk check that it turns out actually does exist

* Updates requirements to mention bizhawk 2.7 instead of 2.3.1

* Fixes off-by-one error in command byte counts

* Fixes program color indices

* Fixes newline PEP violations

* Reverts an accidental whitespace change made to launcher.py

* Fixes URL formatting on link to settings from setup guide

Co-authored-by: Zach Parks <zach@alliware.com>

* Splits several lines in the readme to avoid excessive length

* Fixes formatting and (hopefully) reduces cringe of joke in setup doc

* Removes unnecessary constructor

* Changes item frequency generation to avoid reusing the same references

Co-authored-by: Zach Parks <zach@alliware.com>

---------

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
Co-authored-by: Zach Parks <zach@alliware.com>
2023-06-29 13:36:01 -05:00
Alchav
776b5fab7c LTTP/SM/SMZ3: Show correct item icon for cross-game items (#1112)
Co-authored-by: lordlou <87331798+lordlou@users.noreply.github.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2023-06-29 17:47:21 +02:00
Fabian Dill
18e0d25051 Factorio: fix resync not reconciling divergent history 2023-06-29 15:15:12 +02:00
el-u
dfb3df4a8f lufia2ac: coop support + update AP version number to 0.4.2 (#1868)
* Core: typing for async_start

* CommonClient: add a framework for clients to subscribe to data storage key notifications

* Core: update version to 0.4.2

* lufia2ac: coop support
2023-06-29 08:06:58 -05:00
lordlou
d0db728850 SM: 0.4.1 Fixes and Additional Objective Options (#1859)
* first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions)

* first working single-world randomized SM rom patches

* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions)

* first working single-world randomized SM rom patches

* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* Fixed multiworld support patch not working with VariaRandomizer's

Added stage_fill_hook to set morph first in progitempool
Added back VariaRandomizer's standard patches

* + added missing files from variaRandomizer project

* + added missing variaRandomizer files (custom sprites)

+ started integrating VariaRandomizer options (WIP)

* Some fixes for player and server name display

- fixed player name of 16 characters reading too far in SM client
- fixed 12 bytes SM player name limit (now 16)
- fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO)
- request: temporarly changed default seed names displayed in SM main menu to OWTCH

* Fixed Goal completion not triggering in smClient

* integrated VariaRandomizer's options into AP (WIP)

- startAP is working
- door rando is working
- skillset is working

* - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off")

* skillset are now instanced per player instead of being a singleton class

* RomPatches are now instanced per player instead of being a singleton class

* DoorManager is now instanced per player instead of being a singleton class

* - fixed the last bugs that prevented generation of >1 SM world

* fixed crash when no skillset preset is specified in randoPreset (default to "casual")

* maxDifficulty support and itemsounds removal

- added support for maxDifficulty
- removed itemsounds patch as its always applied from multiworld patch for now

* Fixed bad merge

* Post merge adaptation

* fixed player name length fix that got lost with the merge

* fixed generation with other game type than SM

* added default randoPreset json for SM in playerSettings.yaml

* fixed broken SM client following merge

* beautified json skillset presets

* Fixed ArchipelagoSmClient not building

* Fixed conflict between mutliworld patch and beam_doors_plms patch

- doorsColorsRando now working

* SM generation now outputs APBP

- Fixed paths for patches and presets when frozen

* added missing file and fixed multithreading issue

* temporarily set data_version = 0

* more work

- added support for AP starting items
- fixed client crash with gamemode being None
- patch.py "compatible_version" is now 3

* commited missing asm files

fixed start item reserve breaking game (was using bad write offset when patching)

* Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it).

fixed crash in SMClient when loosing connection to SNI

* fixed No Energy Item missing its ID

fixed Plando

* merge post fixes

* fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color)

* fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses)

* fixed start item x-ray HUD display

* Fixed start items being sent by the server (is all handled in ROM)

Start items are now not removed from itempool anymore
Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though.
Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified

* fixed settings that could be applied to any SM players

* fixed auth to server only using player name (now does as ALTTP to authenticate)

* - fixed End Credits broken text

* added non SM item name display

* added all supported SM options in playerSettings.yaml

* fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region

did some cleaning (mainly reverts on unnecessary core classes

* minor setting fixes and tweaks

- merged Area and lightArea settings
- made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating
- fixed inverted layoutPatch setting

* added option start_inventory_removes_from_pool

fixed option names formatting
fixed lint errors
small code and repo cleanup

* Hopefully fixed ROR2 that could not send any items

* - fixed missing required change to ROR2

* fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum)

* fixed typo with doors_colors_rando

* fixed checksum

* added custom sprites for off-world items (progression or not)

the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu

* - added missing change following upstream merge

- changed patch filename extension from apbp to apm3 so patch can be used with the new client

* added morph placement options: early means local and sphere 1

* fixed failing unit tests

* - fixed broken custom_preset options

* - big cleanup to remove unnecessary or unsupported features

* - more cleanup

* - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips

- small cleanup

* - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch)

* fixed g4_skip patch that can be not applied if hud is enabled

* - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette)

* - updated basepatch to reflect g4_skip removal

- moved more asm files to SMBasepatch project

* - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed)

* fixed wrong path if using built as exe

* - cleaned exposed maxDifficulty options

- removed always enabled Knows

* Merged LttPClient and SMClient into SNIClient

* added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service

* small doc precision

* - added death_link support

- fixed broken Goal Completion
- post merge fix

* - removed now useless presets

* - fixed bad internal mapping with maxDiff

- increases maxDiff if only Bosses is preventing beating the game

* - added support for lowercase custom preset sections (knows, settings and controller)

- fixed controller settings not applying to ROM

* - fixed death loop when dying with Door rando, bomb or speed booster as starting items

- varia's backup save should now be usable (automatically enabled when doing door rando)

* -added docstring for generated yaml

* fixed bad merge

* fixed broken infinity max difficulty

* commented debug prints

* adjusted credits to mark progression speed and difficulty as Non Available

* added support for more than 255 players (will print Archipelago for higher player number)

* fixed missing cleanup

* added support for 65535 different player names in ROM

* fixed generations failing when only bosses are unreachable

* - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish

* fixed failling generations when using 'fun' settings

Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings

* fixed debug logger

* removed unsupported "suits_restriction" option

* fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP)

* - fixed deathlink emptying reserves

- added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves

* - merged death_link and death_link_survive options

* fixed death_link

* added a fallback default starting location instead of failing generation if an invalid one was chosen

* added Nothing and NoEnergy as hint blacklist

added missing NoEnergy as local items and removed it from progression

* fixed broken Item links

* fixed failing generation that could happen with Disabled Tourian

fixed shared Location list that could be modified for each world

* added missing force disable of EscapeRando if an escape solution cant be found

* fixed broken animal surprise patches

* prevent receiving items when in the first room of Ceres (message box in mode7 is broken)

* fixed generating with "activate chozo robots" Objective

* added soft reset that saves to initial starting location

reverted code change applied to fix softlocks from comeback checks
reverted forcing all beam local when using door rando

* replaced "save and reset" with "save and fast reload" (using same Start+Select+L+R)

* added documentation about Save and Reload

removed forgotten docstring about forcing beams as local items when using door rando

* fixed frequent failing generation on WebHost (KeyError: 'Kraid')

* added "objectiveRandom", "nbObjective", objectiveList and adapted Objective selection options to better reflect VARIA's.

fixed "collect 100% items" not being excluded when objectiveRandom is used
added Exception when VARIA initial layout fails

* fixed broken non-AP items

fixed determinism caused by the use of a set

* fixed generation failing on Webhost with string as a OptionSet (replaced default with a list of string)

cleaned doc and naming of Objective related Options
2023-06-29 07:51:09 -05:00
Justus Lind
77b0852dca Muse Dash: Add New Game (#1723)
* Alpha 1 Muse dash stuff.

* Add in an option to limit to only base game songs.

* Make all items progression instead of progression_skip_balancing.

* Add in extra_goal_song_items to help make runs less about completing every song.

* Change ID range to be in a more open area, and add some comments.

* Add in Streamer Mode and difficulty range options. Rearrange data files so its easier to get all data at once.

* Fix generation issues.

* Fix up the maximum and remove old option.

* Remove empty items and the option to make filler songs empty.

* Support emerald hunt mode. Make difficulties an option rather than 2 sliders.

* Fix DLC Song option being inverted.

* Fix item counting being broken if there was more than 1 world.

* Make compatible with .apworld specification.

* Make All item names ASCII compatible.

* Add in the additional_item_percentage option.

* Add a test to ensure the item names are within the normal ascii range.

* Add in death link.

* Remove the album from the item name. Not really needed anymore.

* Add the 2 budget is burning albums under the free songs heading. Adds a couple more songs without dlc.

* Sanitise Album names.

* Added the grade needed choice.

* Update songs to v3.1.0

* Adjust difficulty ranges. Add Expert and Master.

* Fix setup_en.md being out of date.

* Add a manual override.

* Add testing for diff ranges. Fix bugs introduced there. Limit option to 11 to not generate an impossible seed.

* Remove regions from Muse Dash.

* Some Oops...

* Attempt to make tests happy.

* Remove supports weighting false to stop webhost test failing.

* Adjusted settings

* Adjust music sheets to use percentages. Various cleanups.

* Fixes to new code.

* Add Ola Dash Album. Add support for overriding song difficulty. Other stylisation changes.

* Attempt fix tests.

* Ooops missed one.

* flake8 suggestions.

* Remove FM 17314 SUGAR RADIO as that song is a bit weird.

* Update document pages.

* Add trap support

* Lower additional song count by 10.

* Tests broke on my end. Using github to test this.

* Looks like I was accidentally adding ~.

* Fix the one song that crashes OoT hint generation

* Various documentation changes.

* Website documents fixup.

* Doc updates part 2.

* Oops. Doc updates part 3.

* Add Muse Dash to the apworld list.

* Add trailing comma.

* Add a couple plando options.

* Set data_version to 1.

* Add in some handling incase someone decides a song is both starter and included.

* Remove brackets around ifs.

* Oops. Accidentally removed a necessary bracket.

* Fix filtering crash due to me mixing up c# and python .remove().

* Add Happy Otaku Pack Vol.17. Also increment data version.

* Update links to melon loader to be the latest.

* Clean up song selection code by shuffling once then popping.

* Add UID to the Data text file, so the same file can be used client and server.

* Increment Data Version because some names have changed.

* Correct some names.

* Update data to v3.4.0 (Addition of Muse Radio FM104)

* Add support for SFX traps. Adjusted how traps were setup a bit.

* Update the docs to include a troubleshooting section.

* Small fixes.

* Remove unnecessary brackets.

* Add .net downloads to docs.

* Avoid failing generation if strict difficulty settings are applied with no dlc songs and streamer mode.

* Forgot to add the worst starting song count.

* Make minimum song count be Starting Songs + 11 instead of Starting Songs * 2 + 1.

* Fix up several issues where song count could mismatch the requested amount.

* Add a test to ensure world size doesn't grow.

* Fix some oversights.

* Remove unnecessary brackets.

* Fix up passing the tuple out when just the key would suffice.

* Adjust typing based on Phar's suggestions.

* Apply the rest of Phar's suggestions with minor tweaks to other parts to suit suggestions.

* Adjust some more stuff to fit 120 characters.

* Some more pep8 stuff and fix tests.

* Some pep8 in tests.
2023-06-29 07:36:39 -05:00
Aaron Wagener
3fba94f000 The Messenger: strip generated filler items for a sufficiently small pool (#1907)
* The Messenger: strip generated filler items for a sufficiently small remaining item pool

* rewrite the test for the small chance there's no large currency shards
2023-06-29 07:33:37 -05:00
William Quelho Ferreira
85582b9458 TLoZ: fix LauncherComponents entry (#1908) 2023-06-28 19:06:45 -05:00
Aaron Wagener
122d404145 Docs: rework main ap setup guide (#1853)
* rework main ap setup guide

* review updates

* add blurb about re-opening rooms and user-content

* more review suggestions

* remove dead links. Windows blurb
2023-06-28 19:06:18 -05:00
Aaron Wagener
07e3fbe845 Docs, LTTP: clarify not using qusb and remove redundancies (#1373)
* clarify not using qusb and remove redundancies

* SNES mini note

* review suggestions

* remove remaining repetitive text

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-06-28 00:11:06 -05:00
Kory Dondzila
76cace725b WebHost: Fixes multi-tracker checks sorting. (#1893) 2023-06-27 20:40:29 -05:00
NewSoupVi
99656bf059 The Witness: Utils.cache_argsless -> functools.lru_cache (#1897)
* Changed Utils.cache_argsless to functools.lru_cache

* Revmoed unused variable

* Removed remaining direct reference to a .txt outside utils

* Update worlds/witness/utils.py

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>

---------

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2023-06-28 02:23:50 +02:00
Aaron Wagener
332eab9569 The Messenger: Add Shop Rando (#1834)
* add shop shuffle options and items

* add logic for the shop slots

* write cost tests

* start on shop item logic

* make strike and second wind early items

* some cleanup

* remove 5 shards

* double cost requirement for really expensive items and raise the rates

* add test for shop shuffle with minimum other locations

* put power seal in front of shards

* rename locations and items

* update rules, regions, and shop

* update tests and misc fixes

* minor cleanup

* implement money wrench and figurines

* clean out now unneeded info from slot_data

* docs update and fix a failure when not shuffling shops

* remove shop shuffle option

* Finish out shop rules

* make seals generation easier to read and fix tests

* rule adjustments

* oop

* adjust the prices to be a bit more generous

* add max price to slot data for tracker

* update the hard rules a bit

* remove unnecessary test

* update data_version

* bump version and remove info for fixed issues

* remove now unneeded assert

* review updates

* minor bug fix

* add a test for minimum locations shop costing

* minor optimizations and cleanup

* remove whitespace
2023-06-28 01:39:52 +02:00
zig-for
8c2584f872 LADX: 16 bits for the check ID (#1903) 2023-06-27 16:39:57 -05:00
PoryGone
1ced726d31 SA2B: v2.2 Content Update (#1904)
* Ice Trap Support

* Support Animalsanity

* Add option for controlling number of emblems in pool

* Support Slow Trap

* Support Cutscene Traps

* Support Voice Shuffle

* Handle Boss Rush goals

* Fix create item reference to self.multiworld

* Support Ringlink

* Reduce beep frequency to 20

* Add Boss Rush Chaos Emerald Hunt Goal

* Fix Eternal Engine - Pipe 1 logic

* Add Chao voice shuffle

* Remove unused option

* Adjust wording of Required Cannon's Core Missions

* Fix incorrect region assignment

* Fix incorrect animal logics

* Fix Chao Race tooltip

* Remove Green Hill Animal Location

* Add Location Count info to tooltips

* Don't allow M4 first if animalsanity is active

* Add Iron Boots to Standard Logic Egg Quarters 5

* Make Vanilla Boss Rush actually Vanilla

* Increment Mod Version

* Increment Data Package Version

---------

Co-authored-by: RaspberrySpaceJam <tyler.summers@gmail.com>
2023-06-27 16:38:58 -05:00
Freya Arbjerg
d51e0ec0ab WebHost: Align multitracker status column to the left (#1645)
* Align multitracker status column to the left

* Move 'Status' column to after 'Game' column
2023-06-27 17:37:01 -04:00
Felix R
36b5b1207c Add Bumper Stickers (#811)
* bumpstik: initial commit

* bumpstik: fix game name in location obj

* bumpstik: specified offset

* bumpstik: forgot to call create_regions

* bumpstik: fix entrance generation

* bumpstik: fix completion definition

* bumpstik: treasure bumper, LttP text

* bumpstik: add more score-based locations

* bumpstik: adjust regions

* bumpstik: fill with Treasure Bumpers

* bumpstik: force Treasure Bumper on last location

* bumpstik: don't require Hazard Bumpers for level 4

* bumpstik: treasure bumper locations

* bumpstik: formatting

* bumpstik: refactor to 0.3.5

* bumpstik: Treasure bumpers are now progression

* bumpstik: complete reimplementation of locations

* bumpstik: implement Nothing as item

* bumpstik: level 3 and 4 locations

* bumpstik: correct a goal value

* bumpstik: region defs need one extra treasure

* bumpstik: add more starting paint cans

* bumpstik: toned down final score goal

* bumpstik: changing items, Hazards no longer traps

* bumpstik: remove item groups

* bumpstik: update self.world to self.multiworld

* bumpstik: clean up item types and classes

* bumpstik: add options
also add traps to item pool

* bumpstik: update docs

* bumpstik: oops

* bumpstik: add to master game list on readme

* bumpstik: renaming Task Skip to Task Advance
because "Task Skip" is surprisingly hard to say

* bumpstik: fill with score on item gen
instead of nothing (nothing is still the default filler)

* bumpstik: add 18 checks

* bumpstik: bump ap ver

* bumpstik: add item groups

* bumpstik: make helper items and traps configurable

* bumpstik: make Hazard Bumper progression

* bumpstik: tone final score goal down to 50K

* bumpstik: 0.4.0 region update

* bumpstik: clean up docs
also final goal is now 50K or your score + 5000, whichever is higher

* bumpstik: take datapackage out of testing mode

* bumpstik: Apply suggestions from code review

code changes for .apworld support

Co-authored-by: Zach Parks <zach@alliware.com>

---------

Co-authored-by: Zach Parks <zach@alliware.com>
2023-06-27 15:37:17 -05:00
Fabian Dill
a4e485e297 Launcher: keep alive (#1894) 2023-06-27 09:30:54 +02:00
kindasneaki
a7bc8846cd RoR2: bug fixes (#1891)
* adding back parens that got deleted by accident.

* Void Locus and The Planetarium ids backwards

* change required client version

* beads of fealty was missing for A Moment, whole victory

* found another logic bug

* Update worlds/ror2/__init__.py

Co-authored-by: Zach Parks <zach@alliware.com>

* Remove unnecessary comment

---------

Co-authored-by: Zach Parks <zach@alliware.com>
2023-06-26 23:47:52 -05:00
Fabian Dill
125ee8b198 WebHost: fix dict lookup exceptions 2023-06-27 04:39:21 +02:00
Mewlif
553fe0be19 Undertale for AP (#439)
Randomizes the items, and adds a new item to the pool, "Plot" which lets you go further and further in the game the more you have.

Developers: WirTheAvali (Preferred name for professional use, mewlif)
2023-06-27 04:35:41 +02:00
Zach Parks
71bfb6babd Generate: Add skip progression balancing argument. (#1876) 2023-06-26 16:14:01 -05:00
James Groom
1698c17caa Docs: Revise all docs mentioning Lua in EmuHawk (which are in English), and other misc. corrections (#1782)
* Fix links to TASVideos.org using HTTP

* Revise all docs mentioning Lua in EmuHawk which are in English

resolves TASEmulators/BizHawk#3650

* Correct capitalisation of "BizHawk"

in strings and camelCase identifiers

* Use the term "EmuHawk" when referring to the app, in English docs

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-06-26 08:53:44 +02:00
black-sliver
751e5cec63 Ori: fix py3.8 apworld compatibility 2023-06-26 08:16:56 +02:00
NewSoupVi
dc46e96e3f Witness: APworld compatibility, but for real this time (#1896)
* removed relative imports from outside the witness package

* Remove Witness from the apworld shame list
2023-06-26 00:38:39 +02:00
NewSoupVi
0934e5c711 The Witness: Fixed seeds not generating with vanilla logic (#1895)
Yikes, I swear I ran like 15 generations with a random yaml, I got so unlucky
2023-06-26 00:20:28 +02:00
Fabian Dill
aa8ffa247d Setup: flip apworld list (#1882)
* Setup: flip apworld list

* Update setup.py

Co-authored-by: kindasneaki <ryandj67@hotmail.com>

* Update setup.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* setup: make TLoZ an apworld

This reverts commit fd026c5eb2.

---------

Co-authored-by: kindasneaki <ryandj67@hotmail.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-06-25 03:47:38 +02:00
black-sliver
a45e8730cb Fill: fix fill_restrictive for mixed minimal and non-minimal and test (#1800)
* Tests: add test for mixing minimal and non-minimal

* Tests: minor cleanup in test_minimal_mixed_fill

* fix fill_restrictive for mixed minimal/non-minimal

The reason why this only happens for minimal is because it would not accept the solution it found otherwise.
Tracking and releasing unreachable items would be the better solution, but that's a lot harder to do.

* fix typo in fill_restrictive

* fix pep8 in fill_restrictive

* Fill: cleanup invalid unsafe placements, better comments

* Fill: more cleanup
2023-06-25 02:55:13 +02:00
Fabian Dill
46f2f3d7cd Factorio: Client in folder, TextClient: always available (#1829)
* Factorio: move Client into world folder

* Factorio: declare Client as Client Component

* FactorioClient: use centralized launch_subprocess

* TextClient: make always available
2023-06-25 02:31:25 +02:00
black-sliver
a96ff8de16 Linux: add freeze_support, Launcher: use spawn (#1890) 2023-06-25 02:24:43 +02:00
agilbert1412
f3e2e429b8 DLC Quest: Option Documentation improvements (#1887) 2023-06-25 02:13:33 +02:00
NewSoupVi
46b13e0b53 Witness: apworld support (#1885) 2023-06-25 02:00:56 +02:00
t3hf1gm3nt
7a4e903906 TLOZ: APworld support (#1884)
- Remove a relative import in Rules.py
- Clean up a few unused imports in __init__.py
- Use pkgutil instead of open when applying base patch
- make sure rom_name is initialized correctly in modify_multidata

* use os.path.join() instead of explicit "/"
2023-06-25 01:58:54 +02:00
Aaron Wagener
f1ccf1b663 reenable ping 2023-06-25 01:24:39 +02:00
NewSoupVi
ec0822c5eb Docs: Mention Git in the "Optional" section of "Running from Source" (#1880)
* Docs: Mention Git in the "Optional" section of "Running from Source"

GIt is required to install the Zilliandomizer package.

Also, this is probably just nice to have.

* Remove mention of Zillion so the text doesn't need updating.

* Update docs/running from source.md

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update docs/running from source.md

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Mention PyCharm's git integration

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-06-24 12:59:14 +02:00
Fabian Dill
78b981228a Generate: improve error message for missing game (#1857)
---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-06-23 10:17:35 +02:00
NewSoupVi
f3c788d0cc Witness: Fix missing location
All Pressure Plates puzzles are now always locations.

This makes a line where a Pressure Plates location gets added and a different one cause incorrect behavior.
2023-06-23 10:16:39 +02:00
Zach Parks
59ad9e97e5 WebHost: Fix special-range value setting to custom when randomization is toggled off (#1856)
* WebHost: Fix custom-range value setting to `custom` when randomization is toggled off

* Remove redundant code

* Add optional parameter default
2023-06-22 22:12:22 -04:00
StripesOO7
abd8eaf36e WebHost: Change default spoiler-option for games generated from WebHost to 3 instead of 0 (#1852)
* Change default spoiler-option in WebHostLib/generate.py to 3 instead of 0

* shifting spoiler-default to the JS calls instead of setting it in generate.py

---------

Co-authored-by: StripesOO7 <54711792+StripeesOO7@users.noreply.github.com>
2023-06-22 21:01:09 -05:00
black-sliver
f36468fc25 Docs: add info about maintaining worlds (#1838)
* Docs: add info about mainting worlds

* Docs: fix typos in world maintainer

* Docs: commit suggestions into world maintainers

Thanks Joethepic and Silvris

* Docs: fix more typos in world maintainer

* Docs: more typos

* Docs: world maintainers link to core maintainers

* Docs: world maintainers voting on discord

* Docs: add 'world maintainer' link to 'adding games'

* Docs: unmaintained worlds in 'disabled'

* Docs: world maintainer update from review

Thanks LegendaryLinux

* Doc: rephrase world maintainer voting
2023-06-22 08:51:02 +02:00
black-sliver
a939f50480 Clients: use certifi (#1879)
* Clients: use certifi for wss

On Windows, the local cert store might be outdated and refuse connection to some servers.

* Clients: lazily create ssl_context
2023-06-22 00:01:41 +02:00
Fabian Dill
b04b105bd8 LADX: use custom collect/remove to keep track of logical rupee counts instead of LogixMixin
May contain some pep8, sorry
2023-06-21 12:42:11 +02:00
NewSoupVi
845502ad39 The Witness: Hint distribution changes, added locations, misc fixes (#1785)
Changes:

* Hints should feel a lot less same-y now ("Priority hints" are no longer always hints in disguise)
* Keep Hedge Mazes 1-3 and Pressure Plates 1-3 are added as locations in all settings
* Desert Final Room Hexagonal & Desert Final Room Bent 3 are added as locations
* Entries in exclude_locations that are referring to panels are now sent through slot data. This means they can be pre-skipped on the client side.

Fixes:

* Logic error in the Stoneworks that led to more restrictive seeds than necessary
* Logic error for Theater Flowers EP that led to more restrictive seeds than necessary
* Fixed crash in plando when "item" is a dict with weights
* Spoiler log locations were in random order per region, now they are consistent
2023-06-21 00:45:26 +02:00
Sunny Bat
afe9e12ef4 Raft: Fix item prefilling (#1878) 2023-06-20 09:14:46 +02:00
Fabian Dill
a75159b57e WebHost: import Markup from markupsafe (#1848) 2023-06-20 01:01:42 +02:00
Fabian Dill
61fc80505e Core: refactor some loading mechanisms (#1753)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-06-20 01:01:18 +02:00
Fabian Dill
25f285b242 Launcher: deprecate FUNC Component type (#1872)
* Launcher: add hidden component type

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-06-19 09:57:17 +02:00
Fabian Dill
c4e28a8736 Setup: pin cx-Freeze to latest working version 2023-06-19 00:40:14 +02:00
Fabian Dill
422ccdaa4c WebHost: remove some unused imports 2023-06-18 22:56:55 +02:00
Bicoloursnake
1e7c650159 Docs: Updating the macOS guide for 'New Terminal at Folder' (#1865)
* Update mac_en.md

Added an alternate option to simply terminal navigation

* Update worlds/generic/docs/mac_en.md

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-06-18 13:21:12 +02:00
Aaron Wagener
ab64173600 SoE/SNIClient: auto launch SNI before browser when SNIClient patched (#1861)
* auto launch SNI before browser

* launch emulator too :)

* don't infinitely await sni connection
2023-06-18 11:27:08 +02:00
Fabian Dill
36499b8983 Setup: delete outdated Enemizer and SNI files 2023-06-17 00:56:13 +02:00
NewSoupVi
923ff033b1 The Witness: Logic Fix: Vanilla First Wooden Beam (#1867) 2023-06-15 00:30:50 +02:00
Sunny Bat
599d0ac81b Raft: Small website/code touchups (#1866)
* Remove unnecessary Set

* Ocean theme

* Use create_items instead of generate_basic
2023-06-15 00:30:14 +02:00
Ziktofel
ce2433b247 SC2: Python 3.11 compatibility (#1821)
Co-authored-by: Salzkorn
2023-06-12 07:41:53 +02:00
Scipio Wright
f6cb90daf9 Noita: Region connection edits (#1855)
Shifts the Lake region to be connected to The Laboratory, so that the Lake boss is late game instead of early game.
Shifts the Below Lava Lake region to be connected to the Snowy Depths, so instead of being early game it's early-mid game (since that's when you would be expected to be able to have decent enough digging or a Sädekivi.
2023-06-05 19:32:33 +02:00
el-u
54b200451d Docs: Fix typo in world api.md (#1854) 2023-06-01 22:56:44 -05:00
Exempt-Medic
b98080afee Docs: Update YAML planning guide (#1845)
* [Docs] Update YAML planning guide

Changed wording for items accessibility to describe how it actually works. Reordered settings such as local_items and start_location_hints to match their order in templates. Fixed some grammatical errors.

* Fix typo

Co-authored-by: Zach Parks <zach@alliware.com>

* Update doc

Moved `accessibility`, `progression_balancing`, and `triggers` to game sections instead of root sections and reworded description accordingly. Updated version number. Fixed `progression_balancing` values in example YAMLs.

* Indented trigger to be part of ALTTP

---------

Co-authored-by: Zach Parks <zach@alliware.com>
2023-06-01 22:33:12 -05:00
Exempt-Medic
5401e485aa Blasphemous: Logic fixes for WotBC Cherub and Jondo upper west tree root (#1835) 2023-06-01 03:52:46 +02:00
Fabian Dill
58cf9783eb Tests: make names more unique 2023-06-01 01:45:24 +02:00
Fabian Dill
fad0fe16f4 Tests: sort custom loaded tests (#1851) 2023-06-01 01:44:54 +02:00
kindasneaki
c2884e9eb0 RoR2: Victory Conditions Doc Update (#1833) 2023-05-31 18:38:03 -05:00
Doug Hoskisson
1809823308 Zillion: cache key includes gun requirement (#1846)
The key for the logic cache was missing some important information, so it was yielding a cache hit when it should have been a miss.
2023-05-31 05:56:23 +02:00
lordlou
df7462efcc SMZ3 decoding fix (#1847) 2023-05-30 03:05:05 +02:00
FlySniper
00e3c44400 Wargroove: Fixed commander.json file never being closed by the mod (#1841)
The Wargroove mod didn't close the commander.json's file handle. The Wargroove mod will now close that file handle. The change for the mod can be viewed here: FlySniper/WargrooveArchipelagoMod@fc9aeb3
The change can be verified as present in this repository by viewing the binary data in the modAssets.dat file and searching for "commander.json"
2023-05-29 20:33:35 +02:00
agilbert1412
abf4b3bcbc Stardew valley: Fix package and imports for apworld linux (#1842)
- Fix csv load to use explicitly imported self package instead of keyword __package__
- Fix init.py having a relative import to outside of the apworld
2023-05-29 01:00:33 +02:00
Fabian Dill
c9f217943e LttP: fix patching crash if old always_apply adjuster settings were applied 2023-05-25 14:08:56 +02:00
Fabian Dill
e9f8b1ed28 WebHost: use Py3.11 compatible ponyorm 2023-05-25 14:07:21 +02:00
el-u
c46d8afcfa Core: clean up BaseClasses a bit (#1731) 2023-05-25 01:24:12 +02:00
ScootyPuffJr1
f4d9c294a3 [SM] Minor update to link in Options.py (#1831) 2023-05-23 16:30:39 +02:00
Exempt-Medic
42d8fb8409 [Blasphemous] Various logic fixes (#1830)
This makes a few changes to logic to better match the 1.3 rando's logic. This fixes instances where the wrong items were expected, fixes a typo of "Lorqiana", moves the expert logic on "PotSS: Second area ledge" to only apply if on expert, and adds a new route to "DC: Mea Culpa altar" via Linen of Golden Thread + Three Gnarled Tongues
2023-05-22 19:03:21 +02:00
axe-y
127d4812b5 DLCQuest: Fix Documentation Broken Link 2023-05-21 15:48:56 +02:00
Fabian Dill
527f30d91a Core: log race mode enabled 2023-05-21 05:02:14 +02:00
Fabian Dill
1d565b9aaf WebHost: add game to template export 2023-05-21 05:01:56 +02:00
Fabian Dill
6814bc158a WebHost: index columns used by landing page. 2023-05-21 05:01:29 +02:00
alwaysintreble
e80f3206b6 The Messenger: override start_inventory description (#1695)
* The Messenger: override start_inventory description

* use StartInventoryPool directly
2023-05-21 02:54:50 +02:00
el-u
54ea917c48 CI: treat all files as modified on new branches (#1826) 2023-05-20 21:57:38 +02:00
Exempt-Medic
5e9bf4b007 Docs: Update world api excluded/priority locations description (#1807)
* Update world api doc

Changed the description of excluded and priority locations to match how they appear in other places such as the options api doc

* Update world api.md
2023-05-20 20:04:26 +02:00
Fabian Dill
c8453035da LttP: extract Dungeon and Boss from core (#1787) 2023-05-20 19:57:48 +02:00
Fabian Dill
a2ddd5c9e8 LttP: deterministic shop_shuffle 2023-05-20 19:43:44 +02:00
Fabian Dill
97ba631b80 Core: update modules 2023-05-20 19:36:55 +02:00
black-sliver
be4c597c8d Logging: make sure level is applied for websockets 2023-05-20 19:27:12 +02:00
black-sliver
324d3cf042 Main: add __all__ and change wrong imports (#1824)
* Main: add __all__ and change wrong imports

* Adjusters: fix __version__ import
2023-05-20 19:21:39 +02:00
Fabian Dill
b1c5456d18 Subnautica: move mod exports to own module 2023-05-20 18:34:22 +02:00
Cybrou
f474b81f40 LADX: Add --no-magpie argument for disabling magpie bridge (#1788) 2023-05-20 15:30:33 +02:00
el-u
5255bc5cd8 CI: add a workflow to show flake8/mypy violations in modified files of a PR (#1513)
* CI: add a workflow to show flake8 violations in modified files of a PR

* modify a file to trigger the lint check

* CI: add a workflow to show mypy violations in modified files of a PR

* modify a file to trigger the type check

* Split flake8 and mypy into two parallel jobs; run a variant of the workflow on push event; modify a file to trigger the push workflow

* fail the task if there are syntax errors; remove old lint workflow
2023-05-20 14:40:51 +02:00
Exempt-Medic
18127a75f5 Blasphemous: Fixed logic errors in WotHP 2023-05-19 11:05:52 +02:00
espeon65536
899de428df ALttP: fix dungeon fill failures properly (#1812) 2023-05-18 15:31:12 +02:00
Fabian Dill
f401702e7c Core: skip ModuleUpdate in subprocess 2023-05-18 15:29:17 +02:00
JaredWeakStrike
68bfe1705d KH2: AntipointReset (#1815) 2023-05-18 15:28:35 +02:00
Fabian Dill
98b0bf7456 LttP: use local_early_items for Small Key HC in Standard Keyshuffle (#1799) 2023-05-15 09:34:56 +02:00
NewSoupVi
7674e62ba7 The Witness: Logic fix in response to broken seed (Expert Swamp) 2023-05-15 08:54:12 +02:00
zig-for
0b33c25b39 Fix pokemon lua on bizhawk 2.9 (#1794)
---------

Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com>
2023-05-11 17:52:29 +02:00
Scipio Wright
62f4e62d71 Docs: Add location count specifics to Noita (#1805)
Added specifics about the number of checks in the pool.
2023-05-10 17:49:05 +02:00
espeon65536
48add4687c alttp: remove triforce during dungeon item fill (#1801)
This ensures that even for minimal worlds, the locations will be checked appropriately.
2023-05-10 13:06:25 +02:00
JaredWeakStrike
cc08e853a0 KH2: Ability Sync Fix (#1804) 2023-05-10 13:04:43 +02:00
lordlou
7e3fa5058d SM: door color rando option doc (#1803)
Added precision in DoorsColorsRando docstring about beams being forced local items if enabled.
2023-05-09 03:12:24 +02:00
t3hf1gm3nt
c74577d708 [TLOZ] Fix start weapon locations (#1802)
* Fix starting weapon locations usage

Makes a fresh copy of starting weapon locations when get_pool_core is ran
Should fix the issue of dangerous_weapon_locations getting appended to the list for other worlds past the first world that has dangerous StartingPosition, as well as running into the error if ExpandedPool was different between players
Credit for fix goes to @Silvris in the AP Discord
2023-05-08 22:36:35 +02:00
JaredWeakStrike
a8b76b1310 KH2: Async fix and linter cleanup (#1796) 2023-05-07 04:49:37 +02:00
Zach Parks
c8ebad1dfe WebHost: Prevent dict type options with valid_keys from exporting with [] on weighted settings. (#1762)
Thanks HK.
2023-05-06 19:07:57 -05:00
Cyb3R
d3447a3983 Launcher: Fix multiprocessing in built Launcher (#1792) 2023-05-05 00:53:57 +02:00
Fabian Dill
11b2b5ed2f Core: call stages in sorted order (#1791) 2023-05-04 14:14:20 +02:00
Fabian Dill
a0464ecea1 Core: fix start_inventory_from_pool breaking if it's removing the last instance of an item from pool.
Core: fix start_inventory_from_pool removing arbitrary items from pool if quick abort branch is entered.
2023-05-04 03:10:52 +02:00
zig-for
97fd78ba1b LADX: fix bizhawk 2.9 (#1784) 2023-05-03 23:35:14 +02:00
Fabian Dill
a60f370224 Test: fix setting seed from the hash of the seed method, rather than calling it 2023-05-03 04:31:35 +02:00
Scipio Wright
0363630f61 Noita - Docs updates (#1789)
* Docs updates

* Update worlds/noita/docs/setup_en.md

Co-authored-by: Adam Heinermann <aheinerm@gmail.com>

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Adam Heinermann <aheinerm@gmail.com>
2023-05-03 00:35:50 +02:00
black-sliver
39c7c7291e Core: store multidata/multisave enums by value on py3.11 (#1783)
This is what py3.10 did and should be better for AP than the new default
2023-05-02 08:23:39 +02:00
TheLynk
a368520200 Add New Translation for Adventure and Archipidle in french (#1749)
* Add new translation for Adventure and Archipidle in french

Add new translation for Adventure and Archipidle in french

* Add more store in setup page subnautica for more fairness

Add more store in setup page subnautica for more fairness

* tweak update merge #1685 for lua file

tweak update merge #1685 for lua file

* fix text

fix text

* fix wrong translation

fix wrong translation

* Yes it's better

Yes it's better

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2023-05-01 02:03:31 +02:00
Fabian Dill
5d25f908a4 Launcher: fix loading of mcicon (#1779)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-04-30 18:10:58 +02:00
Fabian Dill
9edab76567 WebHost: recycle generator processes 2023-04-30 16:34:03 +02:00
kindasneaki
91b60f2e21 [RoR2] Classic mode logic fix (#1775) 2023-04-29 09:07:42 +02:00
Jarno
41b59488e3 [Docs] Added lua lib (#1751)
* [Docs] Added lua lib

* Update docs/network protocol.md

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-04-29 00:10:43 +02:00
Fabian Dill
42da24cb5e WebHost: offer room owner log download link 2023-04-28 05:31:19 +02:00
zig-for
28c5e9ee65 LADX: Rework dungeon item fill (#1763) 2023-04-28 05:30:13 +02:00
alwaysintreble
b55174ccdf Docs: document option alias in the options doc (#1755)
* Docs: document option alias in the options doc

* give an example of alias and move it under option creation.

* use clearer example names
2023-04-27 09:33:49 +02:00
lordlou
7bcf299412 SM: missing foreign item filter fix (#1774) 2023-04-27 02:23:52 +02:00
Abacys
a7816d186f Launcher: Correcting minor formatting error (#1768)
Reformatting comment to comply with PEP format
2023-04-26 13:43:23 +02:00
Fabian Dill
9d40471dee Subnautica: add free samples option 2023-04-26 10:50:22 +02:00
zig-for
b704070de5 LADX: Fix palettes (#1767) 2023-04-26 10:49:38 +02:00
Fabian Dill
6c459066a7 Core: add generator_version to network protocol 2023-04-26 10:48:57 +02:00
Fabian Dill
4c3eaf2996 LttP: fix that collect can bypass requirements for ganon ped goal (#1771)
LttP: more pep8
2023-04-26 10:48:08 +02:00
TheBigSalarius
bb56f7b400 FF1: Added URange fix for Bizhawk 2.9 support
URange wasn't moved to common.lua (and no longer exists in connector_ff1.lua) when the lua files were changed for Bizhawk 2.9 socket change.
2023-04-26 10:47:25 +02:00
Doug Hoskisson
22ed7ff9c3 Zillion: fix empty 1st Sphere (#1770)
There was a low probability that the Zillion 1st sphere could be empty.
caused this test failure: https://github.com/ArchipelagoMW/Archipelago/actions/runs/4791795268/jobs/8522615992
2023-04-26 07:24:47 +02:00
axe-y
173513c9f4 DLCQuest: Generation bug fix (#1757)
* Fix documentation error

* init_creation

somehow this fix bug

* item_shuffle_fix

also a count as been corrected

* Fix_early_generation

* Update __init__.py

* Update __init__.py

fix version specific bug

* fix rule set for final boss

and did some reformation
(thanks kaito)

* Update Rules.py

the sword trio can now be in itself if before or actually themself

* Core: correct typing info for item_in_locations
Core: rename item_in_locations to item_name_in_location_names
Core: add actual item_name_in_locations

* item_shuffle_fix

also a count as been corrected

* Fix_early_generation

* fix rule set for final boss

and did some reformation
(thanks kaito)

* Update Rules.py

the sword trio can now be in itself if before or actually themself

* Fix the missing []
and switch to the good function

* - Cleanup and Black Sliver's suggestions

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-04-25 09:06:58 +02:00
Bicoloursnake
c0cf35edda StarCraft 2 macOS documentation (#1747)
* Adding SC2 macOS instructions

A few hours ago, I tested whether the client would run successfully on macOS (Send/Receive items, load maps, download maps, etc.). After the successful testing, I thought adding some documentation would be nice for those who want to play Archipelago on a macOS system.

* Don't need sudo

Turns out you don't need sudo to do the download_data, oops.

* Removed Extraneous Parantheses
2023-04-25 00:53:33 +02:00
Fabian Dill
dcc628f878 Core: correct typing info for item_in_locations
Core: rename item_in_locations to item_name_in_location_names
Core: add actual item_name_in_locations
2023-04-24 23:14:13 +02:00
Fabian Dill
b950af09a6 Factorio: remove tech_tree_layout_prerequisites from core 2023-04-24 23:11:25 +02:00
Fabian Dill
58aea7ca58 Multiserver: cleaner exit (#1743) 2023-04-23 22:21:28 +02:00
JaredWeakStrike
06a25a903e KH2: New Unit Test and better keyblade fill (#1744)
__init__:
 - Added exception for if the player has too many excluded abilities on keyblades.
 - Fixed Action Abilities only on keyblades from breaking.
 - Added proper support for ability quantity's instead of 1 of the ability 
 - Moved filling the localitems slot data to init instead of generate_output so I could easily unit test it

TestSlotData:
- Checks if the "localItems" part of slot data is filled. This is used for keeping track of local items and making sure nothing dupes
2023-04-23 22:20:43 +02:00
Alchav
62a265cc31 Pokémon R/B: logic and location name fixes (#1752)
Corrects incorrect name listed for location in rules.py leading to logic rules failing to apply.
Swaps location names for incorrectly-named trainersanity checks in Viridian Gym
2023-04-23 22:17:03 +02:00
lordlou
67c3076572 SM: comeback fix6 and some refactor (#1756)
refactored and cleaned a bit SMWorld class for best practices:
- moved content of Regions.py and Rules.py in SMWorld
- moved appropiate code to their dedicated World core functions
- moved some Entrances being created in generate_basic to create_regions
more comeback check fixes:
- fixed setting progression door openers items local if doors_colors_rando is used
- enable comeback check only for filling stage as later stages (progression balancing, accessibility and spoiler playthrough) are prone to fail with it
2023-04-23 22:16:01 +02:00
agilbert1412
ab5cb7adad Stardew Valley - Add alias for renamed Fishsanity option (#1758) 2023-04-23 21:59:46 +02:00
lordlou
5e84f91d2f SM: comeback fix5 (#1746) 2023-04-22 02:57:31 +02:00
zig-for
a7a17a5a4d Fix OOT? (#1721) 2023-04-20 09:23:04 +02:00
Ziktofel
a6ea3e1953 Sc2wol logic update (#1715)
Fixes some issues with typos and oversights logic generation in SC2

Rebalancing
Adds Vultures to Advanced tactics as train killers
Drops Firebats from basic unit pool and moves Goliaths from advanced tactics to standard
2023-04-20 09:13:34 +02:00
zig-for
0515acc8fe LADX: no pre fill (#1673)
* no pre fill
* redo trade logic
2023-04-20 09:12:53 +02:00
Fabian Dill
bf5c1cbbbf WebHost: add spoiler level field to generate form 2023-04-20 09:12:07 +02:00
alwaysintreble
c8fb46a5e6 The Messenger: Throw error for invalid names and replace _ with (#1728)
* Replace '_' with ' ' and throw error for other invalid names

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2023-04-20 09:11:15 +02:00
lordlou
a38a2903d5 SM: comeback fix4 (#1741) 2023-04-20 09:10:21 +02:00
JaredWeakStrike
4ef7e43521 KH2: client bug fixes (#1742)
Use item index instead of location and player to determine if the player should get the item.
Fixed getting stat increases on the title screen breaking stuff.
Changed local locations list from a list organized by world-id to a set.
Fixed the inventory slots to be the actual back of inventory.
Fixed recaching when not closing the client but switching slots .
Fixed getting a ability faster than the game so it dupes.
Removed verify location since it was never used.
2023-04-20 09:08:59 +02:00
Fabian Dill
e1f17fadfc Launcher: add icons where present and add headers to table 2023-04-20 05:59:49 +02:00
Adam Heinermann
4dc934729d Noita: implement new game (#1676)
* Noita: implement new game (#1676)

---------

Co-authored-by: DaftBrit <87314354+DaftBrit@users.noreply.github.com>
Co-authored-by: l.kelsall@b4rn.org.uk <l.kelsall@b4rn.org.uk>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Scipio Wright <lightdemonjoe4@gmail.com>
Co-authored-by: Zach Parks <zach@alliware.com>
2023-04-19 22:21:56 -05:00
Zach Parks
722757e18a Clique: Better, less-obtuse, docs for Clique. (#1719) 2023-04-19 21:17:16 -05:00
kindasneaki
0ca3c5e6a2 [kivy] change the sizing for macOS (#1732) 2023-04-19 23:16:13 +02:00
agilbert1412
664bbd86bb Stardew Valley: Fix Mistake and Formatting in settings page (#1737) 2023-04-19 23:15:22 +02:00
axe-y
7a9d4272be Generation bug fix (#1740) 2023-04-19 23:14:46 +02:00
alwaysintreble
7559adbb14 LTTP: fix bad parens in logging 2023-04-19 04:32:15 +02:00
Trevor L
1a7bc4ffd4 Blasphemous: Fix logic for Laudes (for real this time) (#1727) 2023-04-18 02:03:06 +02:00
Fabian Dill
be74a4a71a LttP: update xxtea to 3.0.0 2023-04-17 22:56:54 +02:00
lordlou
cb634fa8d4 SM: failing generation fixes (#1726)
- fixed wrong condition in Collect to assign lastAP
- fixed possible infinite loop in generating output when many SM worlds are present
- fixed new VARIA code that changed a list used for every SM worlds and would throw if many SM worlds uses Aea rando and not AreaLayout
2023-04-17 05:46:19 +02:00
axe-y
f6758524d5 DLCQuest: fix loader bug (#1729) 2023-04-17 02:38:48 +02:00
Fabian Dill
f395a6d184 Docs: has_all and has_any (#1725)
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2023-04-16 12:59:53 +02:00
agilbert1412
ea03c90152 Stardew Valley: Fix a logic bug and a documentation typo (#1722)
Typo in the documentation
Logic error for help wanted quests
2023-04-16 11:22:33 +02:00
Trevor L
50d9ab041a Blasphemous: Fix logic for Laudes (#1724) 2023-04-16 10:53:42 +02:00
axe-y
acd3cb45bf DLCQuest: Fix documentation error (#1720) 2023-04-16 05:04:47 +02:00
JaredWeakStrike
8a78062825 KH2: fixed lucky emblem required>lucky emblem amount (#1718) 2023-04-16 01:59:01 +02:00
Fabian Dill
599cd2c82e Launcher: add Discord links and generate yamls (#1716) 2023-04-16 01:57:52 +02:00
espeon65536
89ec31708e oot: bugfixes (#1709)
* oot client: check types of tables coming from lua script for safety
There was a reported bug with corrupted (?) slot data preventing locations sending. This should safeguard against any instances of that happening in the future, if it ever happens again.

* oot: repair minor hint issues
SMW has # in some location names which breaks ootr's poor text formatting system, so those need to be filtered out.
Also replaces "[X] for [player Y]" with "[player Y]'s X" as frequently requested.

* oot: update required client version

* oot client: fix patching filename bug

* oot: fix broken poachers saw item
how was I this stupid, seriously

* oot: sanitize player, location, and item names everywhere
2023-04-16 01:45:31 +02:00
Fabian Dill
ef211da27f Core: update modules 2023-04-16 01:43:24 +02:00
JaredWeakStrike
0122eb38ab KH2: Fix validAbilitys (#1714) 2023-04-15 22:04:08 +02:00
JaredWeakStrike
3d8bc0bb67 KH2: Init Cleanup and Keyblade Fix (#1713) 2023-04-15 21:17:23 +02:00
JusticePS
d85c13ef0e Adventure: Fix error in connector lua that was fairly harmless in BizHawk 2.8 but throws in 2.9 2023-04-15 21:16:11 +02:00
Doug Hoskisson
27cb93d319 Asset: make icon 512 x 512 (#1710) 2023-04-15 10:37:31 -05:00
black-sliver
b0e8c8db6b MultiServer: fix broken ping (#1711)
Ping argument seems to have changed in an update of websockets.
This uses the lib's default of 20s now.
2023-04-15 17:10:33 +02:00
zig-for
5a7d20d393 Lua: Further centralize code, fix Bizhawk 2.9 (#1685)
* Fixes the socket library for bizhawk 2.9/lua 5.4 by including another one in parallel
* Fixes lua 5.4 support by making socket.lua into a "modern" module (the `module` keyword is gone)
* Adds the linux version and 32 bit windows socket dlls because why not
* Merges common functions into `common.lua` - the only functional change of this should be that:
  * Some things that were locals are globals now - this can be changed, I just was lazy and it likely doesn't matter
  * `drawText` now uses middle/bottom for all prints - feel free to do what you like with that change
2023-04-15 09:17:33 +02:00
zig-for
808203a50f LADX: fix egg sprite in non-patched gfx mode (#1699) 2023-04-15 07:47:45 +02:00
zig-for
d8f79b4a42 LADX: Fix useless item being marked as progression (#1700) 2023-04-15 06:48:05 +02:00
zig-for
02ef6cee47 LADX: Support magpie tracker's sendfull button (#1701) 2023-04-15 06:47:36 +02:00
JaredWeakStrike
e716b50f8c KH2: Fixed Non Deterministic Generation (#1707) 2023-04-15 06:15:04 +02:00
agilbert1412
3fdf07677c Stardew Valley: Removed Pytest Requirement from everything (#1696) 2023-04-15 05:42:02 +02:00
t3hf1gm3nt
d3baca9251 [TLOZ] Add FileNotFoundError handling for base rom (#1708) 2023-04-15 05:40:48 +02:00
alwaysintreble
f52ca2571f Tests: Add tests for location name groups (#1706) 2023-04-14 20:11:01 +02:00
Fabian Dill
469807ba01 Core: remove outdated assert on push_item 2023-04-14 20:05:12 +02:00
alwaysintreble
8ada91939c LTTP: Fix Location Name Groups (#1705) 2023-04-14 20:04:20 +02:00
axe-y
054d14baa4 Fix DLCQuest Errors Generating on Latest Source, without any DLCQuest YAMLs (#1704) 2023-04-14 07:09:50 +02:00
Magnemania
f0324e60f8 SC2: Removed extra space from location (#1697) 2023-04-11 11:50:26 -05:00
zig-for
70ff19ac8c LADX: AP egg title screen (#1683) 2023-04-11 09:18:33 +02:00
Fabian Dill
b02b329181 DLCQuest: fix data_version 2023-04-11 08:47:19 +02:00
Nyx-Edelstein
8b7ffaf671 ALTTP: Add "oof" sound customization option (#709)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: Fabian Dill <fabian.dill@web.de>
Co-authored-by: Zach Parks <zach@alliware.com>
2023-04-10 21:31:57 -05:00
toasterparty
c711d803f8 [OC2] Enabled DLC Option (#1688)
- New OC2 option `DLCOptionSet`, which is a list of DLCs whose levels should or shouldn't be used for entrance randomizer (and mention in documentation). By default, DLC owners now need to enable DLCs in weighted settings.
- Throw user-friendly exceptions when contradictory settings are enabled
- Slightly relax generation requirements for sphere 1/2 level permutations
- Write entrance randomizer info in spoiler log
- Skip adding "Dark Green Ramp" to item pool if Kevin Levels are disabled
2023-04-11 03:43:29 +02:00
Zach Parks
3c3954f5e8 Core: Band-aid fixes start_inventory_from_pool causing generation failures if any world doesn't utilize it. (#1694)
* Core: Band-aid fixes `start_inventory_from_pool` causing generation failures if any world does utilize it.

* Core: Slightly better(?) solution

* Set default so it doesn't fail on WebHost.
2023-04-10 20:18:29 -05:00
Alchav
05d398a51d Pokémon R/B: Missing game corner logic (#1693)
* Game corner logic fix

* Fix for the fix
2023-04-10 20:16:38 -05:00
agilbert1412
5eadbc9840 Major Game Update: Stardew Valley v3.x.x - The BK Update (#1686)
This is a major update for Stardew Valley, for version 3.x.x.
Changes include a large number of new features, including Seasons Randomizer, SeedShuffle, Museumsanity, Friendsanity, Complete Collection Goal, Full House Goal, friendship multiplier

Co-authored-by: Jouramie <jouramie@hotmail.com>
2023-04-11 01:44:59 +02:00
Zach Parks
0c1e3097c3 WebHost: Set defaults for lists/sets on Weighted Settings page (#1692) 2023-04-10 18:01:54 -04:00
Fabian Dill
cdf7ca1dcc Core: allow ordered valid keys (#1690)
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
2023-04-10 23:54:56 +02:00
alwaysintreble
77fbd0eb2b MultiServer: Notify clients of hint points (#1548)
* notify clients of their amount of hint points on initial connection and when hinting

* send in connect packet instead of sending a RoomUpdate on connect

* send hint_points update in `on_new_hint`

* add to connected packet docs

* hint_points isn't a new variable on RoomUpdate now

* note roomupdate can contain connected members

* add the hint point stuff to commonclient

* only show hint points when relevant and default to 0

* Revert "note roomupdate can contain connected members"

* remove hint_points from roomupdate args list and condense explanation of possible packet args

* updates from phar's review

* Small tweak to wording in RoomUpdate

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: Phar <zach@alliware.com>
2023-04-10 14:44:20 -05:00
Fabian Dill
c7284f90d9 Core: implement start_inventory_from_pool (#1170)
* Core: implement start_inventory_from_pool

* Factorio/LttP/Subnautica: add start_inventory_from_pool Option
2023-04-10 21:13:33 +02:00
alwaysintreble
8d559daa35 LTTP: use server for counting locations instead of having 249/216 (#1648) 2023-04-10 21:12:06 +02:00
Ziktofel
e49ffc64f2 SC2: Rebalance item classes (#1512)
It's been a month, if Condor disapproves this can be reverted.
I don't really know what to do about the SC2WOL situation going forward.
2023-04-10 21:08:49 +02:00
alwaysintreble
94a02510c0 core: Region management helpers (#761) 2023-04-10 21:07:37 +02:00
Fabian Dill
9d73988030 Tests: check that options have a docstring (#823) 2023-04-10 04:33:47 +02:00
JaredWeakStrike
81411a191c KH2: init cleanup and random visit locking fix and docs update. (#1652)
Random Visit Locking wasn't copying correctly
init cleanup and moved itempool population to create_items
Updated docs due to a lot of people having issues setting it up.
2023-04-10 04:12:23 +02:00
lordlou
6059b5ef66 SM: 20221101 update (#1479)
This adds support to most of Varia's 20221101 update. Notably, added Options for:
- Objectives
- Tourian
- RelaxedRoundRobinCF

As well as previously unsupported Options:
- EscapeRando
- RemoveEscapeEnemies
- HideItems
2023-04-10 00:35:46 +02:00
black-sliver
0bc5a3bc8d Readme: add missing game DLC Quest (#1682) 2023-04-09 15:53:23 -05:00
black-sliver
11fdb29357 Doc: apworlds have to be all lower case
https://discord.com/channels/731205301247803413/731214280439103580/1094655639600508999
2023-04-09 21:48:22 +02:00
axe-y
bbef7a4cbc DLCQuest : implement new game (#1628)
adding DLC Quest as a new game
2023-04-09 21:06:59 +02:00
Fabian Dill
8e7bbb4ea8 Factorio: flatten science pack curve (#1660)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-04-09 20:58:24 +02:00
alwaysintreble
6628e8c85d Docs: Add some more details to running from source doc (#1680)
* make build tools step more obviously optional and give better directions

* review commit
2023-04-09 15:53:14 +02:00
lordlou
84402a1b55 SM and SMZ3 apworld support (#1677) 2023-04-08 22:52:34 +02:00
Fabian Dill
f4035b8621 MultiServer: remove remaining forfeit compat from network layer 2023-04-08 20:10:07 +02:00
Fabian Dill
bbf8546867 MultiServer: Flag for saving on datastore, create_as_hint scout and client state change 2023-04-08 20:09:51 +02:00
Zach Parks
67a22b8b43 Clique: Force priority location for "final location" and other minor tweaks. (#1583)
* Clique: The greatest game of all time

* Fix failing test

* Increment data_version for backwards compat

* Clique: Tweaked names and forced progression for "The Button"

* Clique: Just location/item definitions to class

* Clique: More tweaks.

* Clique: Fix derp moment.

* Clique: Fix derp moment, part dos.

* Clique: Suggested changes.

* Clique: Update domain

* Clique: Create derived classes for Item and Location

* Clique: simplify line
2023-04-07 19:05:16 -05:00
Trevor L
8e6ec85532 Blasphemous: Update docs, item creation (#1670)
* Blasphemous: Update docs, item creation

* Blasphemous: Remove get_pre_fill_items
2023-04-07 19:04:34 -05:00
Yussur Mustafa Oraji
8fc50510a0 sm64ex,v6: Use create_items for itempool modification (#1674) 2023-04-07 19:03:28 -05:00
Trevor L
aa6ad5d34f Hylics 2: Update item creation (#1671) 2023-04-07 20:01:26 +02:00
zig-for
ccb89dd65c WebHost: Fix upload of .archipelago file (#1657)
* Fix upload

* simple fix for slot data

* remove extra patch.data check

* remove extra parens

* Update WebHostLib/upload.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update WebHostLib/upload.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* parse -> process

* Update WebHostLib/upload.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-04-07 00:24:03 +02:00
agilbert1412
a86c0aa37d Stardew: Fix link in Setup Guide (#1672) 2023-04-06 20:07:09 +02:00
zig-for
ece6598b09 LADX: apworld (#1665) 2023-04-06 20:06:34 +02:00
alwaysintreble
eef8f7af1a The Messenger: Add Mega Time Shards and Quest 1 boss locations (#1661)
* implement mega shards

* create the option and locations, add to slot data and tests

* add boss refights as locations

* remove barma'thazel. it's apparently impossible to get to him

* remove barma'thazel again

* up max shard count to 85

* increment version

* dynamically alter the power seal pool

* revert host.yaml change

* two mega shards were missing from the maps

* add new checks to the info page

* add some more rules to skylands

* forgot to update my tests

* explicit imports, remove unnecessary typing, lower required client ver

* use generators for shard and seal creation
2023-04-06 10:48:30 +02:00
Chris Wilson
c626618221 Update ArchipIDLE's documentation and create items in create_items func. (#1669) 2023-04-06 00:08:41 -04:00
kindasneaki
47989325f8 [Webhost] header closing tag moved after mobile menu (#1650)
* Change archipelago mod download page

* Docs: change connecting to archipelago in RoR2 setup guide

* /header off by one
2023-04-05 19:27:56 -04:00
black-sliver
815e7e6b0a Core: default data_version to 0 (#1668)
* Core: default data_version to 0

This allows new (ap-)worlds to function with old clients without having to define a version.

* Blasphemous: fix data_version
2023-04-06 01:19:58 +02:00
Fabian Dill
a61a1f58c6 Network: allow filtering checked and missing by text fragment 2023-04-05 19:36:32 +02:00
black-sliver
4c24872264 CI: update ubuntu to 20.04
18.04 will not be supported starting 2023-04-01
2023-04-05 19:16:32 +02:00
Zach Parks
e778e49574 Core: Fix ZeroDivisionError if a world is contained 100% locked locations. (#1659)
* Core: Fix divide by zero error if all locations are priority

* Core: 100% makes more sense than 0% in this context

* Core: Revert a some "autoformatter" damage

* Core: Move comparisons a bit earlier.

Worlds that are 100% filled with locked locations should now be completely skipped in the progression balancing step now as we filter them out at the very beginning of the stage now. Also skips progression balancing if it turns out all locations are locked early before attempting to balance.
2023-04-05 12:11:34 -05:00
FlySniper
25f7413881 Wargroove: Client is not added to the Start Menu. (#1664)
* Wargroove - Fixed client not showing up in the windows search menu.
2023-04-05 00:07:15 -05:00
Alchav
397ce8343e Pokémon R/B: Pokédex option fixes (#1666)
* Pokémon R/B: Pokedex option fixes

* Pokémon R/B: Missing option display names
2023-04-04 23:59:59 -05:00
Trevor L
37fdc00517 Blasphemous: Small logic fixes (#1667) 2023-04-04 23:54:51 -05:00
alwaysintreble
03aa9b3604 Update AP icons (#1637) 2023-04-04 23:38:23 -05:00
JusticePS
8f52e4654f Adventure: Change user_path to local_path for locating basepatch file (#1639) 2023-04-04 19:32:03 +02:00
alwaysintreble
a86fd37860 Core: set convert_name_groups to true for LocationSet (#1663) 2023-04-04 19:29:20 +02:00
toasterparty
eb503adb13 [OC2] Only Calculate Priority Locations Once (#1662) 2023-04-04 06:53:05 +02:00
Fabian Dill
cbf72becc1 Subnautica: group items and removal of item pool option 2023-04-04 06:46:54 +02:00
alwaysintreble
cdd460ae15 The Messenger: some miscellaneous cleanup. Also found a logic bug. oops. (#1654) 2023-04-04 02:27:36 +02:00
toasterparty
ffd968d89d [OC2] Fix Overworld Access Logic for World 3 (#1655) 2023-04-04 02:26:24 +02:00
toasterparty
8d73746d5b [OC2] Spelling Mistakes (#1656) 2023-04-04 02:25:59 +02:00
zig-for
5ed56db48a LADX: Fix crash in item pick up with > 100 players (#1658) 2023-04-04 02:23:39 +02:00
PoryGone
5f447f4e6b SMW: Fix Option Tooltip (#1651) 2023-04-02 03:54:07 +02:00
Fabian Dill
f015cf4298 MultiServer: compat fix if checksum is not present (#1642) 2023-04-01 22:40:14 +02:00
Freya Arbjerg
e43bb99622 Fix broken styling of multitracker navigation (#1644) 2023-04-01 19:56:08 +02:00
JaredWeakStrike
34de5a57af KH2: updated the docs for options and faq (#1641) 2023-04-01 19:55:43 +02:00
Fabian Dill
510a460d84 Multiserver: location name groups fix (#1643) 2023-04-01 19:54:44 +02:00
Justin LaLone
6e271b643d Adventure: Added examples for automatically loading adventure_connector.lua
in BizHawk
2023-04-01 19:54:02 +02:00
Fabian Dill
8971340a66 Core: update version to 0.4.0 2023-03-31 14:43:05 +02:00
zig-for
30cfd3186c LADX: Fix local paths (#1634) 2023-03-31 14:05:51 +02:00
Chris Wilson
1dc4e2b44b Restore "random" option to weighted-settings (#1635)
* Restore "random" option to weighted-settings, adjust capitalization of hardcoded settings

* Set default value as "random" for Choice, TextChoice, and Toggle options with no default value
2023-03-30 16:01:31 -07:00
alwaysintreble
d5b4a91a13 The Messenger: Have users explicitly remove old version (#1632) 2023-03-30 23:18:56 +02:00
alwaysintreble
bf5282dfa8 add Toggle options back to player settings and remove unnecessary check (#1633) 2023-03-30 16:56:26 -04:00
agilbert1412
4eea91daab Stardew Valley: Improve Documentation (#1631) 2023-03-30 16:25:25 +02:00
Wilhelm Schürmann
20e80d06cf LADX: Add Lua connector for BizHawk (#1579)
This is a Lua script for BizHawk that implements the relevant parts of
the RetroArch networking API used by the Archipelago LADX Client.

socket.lua and core.dll are exact copies of the same files in
data/lua/OOT and various other folders. There is a PR consolidating
these into the base folder, which this commit is anticipating.

LADX "just works"(tm) when this is loaded in Bizhawk.
2023-03-30 15:55:38 +02:00
Rosalie-A
59b78528a9 TLoZ: Fix description and off-by-one error (#1625) 2023-03-30 15:31:16 +02:00
Fabian Dill
cd4fd18706 Core: update modules (#1612) 2023-03-30 15:30:43 +02:00
KernRat
af44c1ba3d Factorio: Fixed outdated energy link troughput in docs (#1626) 2023-03-30 15:30:25 +02:00
Alchav
3ef0a56ec2 Pokémon R/B: Another quiz fix 2023-03-30 15:29:55 +02:00
t3hf1gm3nt
4ff282a384 [TLOZ] Pols Voice Logic Fix (#1630)
* TLOZ: Pols Voice Logic Fix

Was informed that Pols Voice require certain weapons to kill that might not be guaranteed by the starting weapon. This is the only regular enemy in the game that cannot be killed by either any of the starting weapons or bombs which can be bought, so adding this rule should prevent any issues.
2023-03-30 15:29:06 +02:00
Chris Wilson
f3dad894ec Update ArchipIDLE item count to 200, and add a few more items (#1627) 2023-03-29 18:27:35 -07:00
Chris Wilson
a5373e3672 [WebHost] Add support for items-list, locations-list, and custom-list option types to weighted-settings (#1614)
* Add support for items-list, locations-list, and custom-list option types to weighted-settings

* Fix subclass detection for `items-list`, `locations-list`, and `custom-list` in weighted-settings

* Move specially handled options alongside each other in the UI, and split them into logical groupings

* Fix header text and location for Priority an Exclusion locations

* Add universally supported "random" option to `Choice` and `TextChoice` options in weighted-settings

* Update description text for exclusion items to clarify they also prevent helpful items.

* Be technically correct and call them `useful` items.
2023-03-29 14:37:39 -07:00
black-sliver
639606e0be worlds,clients,kvui: use user_path (#1622) 2023-03-29 20:14:45 +02:00
zig-for
bb79073ce7 LADX: Fix autotracking for shop items (#1623) 2023-03-29 14:56:10 +02:00
zig-for
53b3cd029e LADX: Add options for sword music and nag messages (#1621) 2023-03-29 14:48:10 +02:00
zig-for
99bd525c8e LADX: use verify() to verify sprites (#1620) 2023-03-29 14:42:08 +02:00
zig-for
d14ab97849 LADX: Fix gen failures with non-ascii player names, fix missing custom (#1619) 2023-03-29 14:41:11 +02:00
zig-for
f50e85b401 LADX: Fix repeated rupee adds overwriting instead of adding (#1618) 2023-03-29 14:40:19 +02:00
Fabian Dill
b64565594a kvui: block settings menu 2023-03-29 14:39:06 +02:00
JaredWeakStrike
ae7dad8bf9 Fixed Blacklist and python 3.8 support (#1616) 2023-03-28 18:02:06 +02:00
alwaysintreble
b7c74919b7 Spoiler: add player name to sphere 0 items in playthrough (#1607) 2023-03-27 19:42:15 +02:00
Jarno
a7f7f91aaf Timespinner: LOGIC FIX, RC BUG (#1610) 2023-03-27 19:17:50 +02:00
JaredWeakStrike
e62f989ce8 Kh2 rc2 fixes (#1608) 2023-03-27 19:17:06 +02:00
Chris Wilson
21c6c28755 [WebHost] Fix weighted-settings UI incorrectly populating range values (#1602)
* Fix a bug causing weighted-settings UI to incorrectly display range values with no default as having a value of 25.

* Do not set min and max values of range options to zero by default. This causes clutter in saved `.yaml` files.
2023-03-27 08:55:13 -07:00
Alchav
f0403b9c9d Pokémon R/B: Sort fix 2023-03-27 15:28:17 +02:00
Chris Wilson
f09f3663d6 [WebHost] Unify style and behavior of popover and mobile menus (#1596)
* [WebHost] Unify styles for popover and mobile menus. Adjust popover menu to function the same as the mobile menu, and toggle via JS.

* Remove class `first-link` in favor of CSS `:first-child`.

* Adjust mobile menu link font size and padding. Change wording of popover trigger text. Add border-right to popover menu. Change "Upload Host File" to "Host Game"

* Change mobile menu text to "Host Game"
2023-03-27 00:12:10 -04:00
Brooty Johnson
b5bd93c420 fixed duplicate location (#1604)
removed duplicate location for "PW: Vilhelm's Leggings"
2023-03-27 03:04:04 +02:00
Fabian Dill
90813c0f4b Test: explicitly make sure there is no double use of location Name or ID (#1605) 2023-03-26 16:04:13 -07:00
JusticePS
e2c4293a6d Adventure: Fix basepatch access from other working directories (#1600) 2023-03-26 19:34:18 +02:00
Payden K. Pringle
963c33c02a Docs: Update BizHawk Open Script wording to be clearer. (#1599) 2023-03-26 19:34:00 +02:00
black-sliver
7d603e7d8d CI: run build_exe twice (#1598)
* CI: run build_exe twice ...

... to find broken conditional imports

* CI: build again in venv
2023-03-26 00:54:56 +01:00
black-sliver
f2e1495d39 Setup: fix broken cx_freeze import 2023-03-26 00:20:14 +01:00
Tarokarr
7927b2ee25 LADX: Added: Credits for sprite sheets (#1594) 2023-03-25 22:29:59 +01:00
alwaysintreble
4f2b13a674 The Messenger, Docs: Change links for the release (#1595)
* change links to point to my fork

* Add documentation about reconnecting because that's necessary apparently

* change the bug report link

* be non ambiguous

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-25 22:11:51 +01:00
black-sliver
ffd7d5da74 ModuleUpdate/Setup: install pkg_resources, check pip, typing and cleanup (#1593)
* ModuleUpdater/setup: install pkg_resources and check for pip

plus minor cleanup in the github actions

* ModuleUpdate/setup: make flake8 happy

* ModuleUpdate/setup: make mypy happier
2023-03-25 19:54:42 +01:00
Trevor L
67eb370200 Blasphemous: Change ending option (#1592) 2023-03-25 19:37:25 +01:00
JaredWeakStrike
4456e36fbb KH2: fixes medal being shadow archive memory address (#1589) 2023-03-25 19:36:48 +01:00
ScootyPuffJr1
7fd9e71b3c TLOZ: Fix Broken Links to Player Settings Page 2023-03-25 19:36:13 +01:00
alwaysintreble
f4a68f1c3d Core: add an everywhere location group (#1582) 2023-03-25 19:35:19 +01:00
Fabian Dill
754a57cf69 WebHost: give active rooms a chance to reclaim their port 2023-03-25 19:34:09 +01:00
lordlou
384577e421 SM: cx_freeze fix (#1584) 2023-03-25 19:30:38 +01:00
alwaysintreble
0ed3865c30 The Messenger: Fix a missing location rule and missing known issue (#1586)
* fix missing rule

* document a missing known issue

* fix a break when shuffle seals is off

* test the thing i just fixed

* invert the if so it's a bit faster
2023-03-25 00:55:24 +01:00
zig-for
77b2ed54a6 LADX: Fix D6 keylogic (#1585)
* fix keylogic for d6

* markup required keys for keylogic

* add test

* Update __init__.py
2023-03-25 00:23:42 +01:00
PoryGone
0386d9f6d2 Fix SA2B Option Display Name (#1591) 2023-03-25 00:22:47 +01:00
Fabian Dill
7e52b6d8bb MultiServer: if there is a hint cost, don't make it 0 (#1581) 2023-03-24 23:14:34 +01:00
kindasneaki
03cf525b2c Webhost: Add dropdown menus (#1553)
* Change archipelago mod download page

* Docs: change connecting to archipelago in RoR2 setup guide

* dropdowns for links

* change some relative sizing

* change links and reorder links

* dropdowns for links

* change some relative sizing

* change links and reorder links

* mobile view was showing on desktop early

* add in missing relative font sizes

* clean up and add a temp downdown img

* move links around

* added cloud border

* move arrow to the left side
2023-03-23 22:11:39 -07:00
zig-for
e1f46d623c LADX: Pass in seed_name and auth separately (#1575) 2023-03-23 21:23:58 +01:00
zig-for
5bb6ff0ce0 LADX: Fixup missing descriptions (#1576) 2023-03-23 21:22:42 +01:00
zig-for
256f493ada LADX: fix web gen (#1574) 2023-03-23 14:53:48 +01:00
Chris Wilson
3ec2d45f4f [WebHost] Improve mobile styles for WebHost (#1571)
* Improve mobile styles for WebHost

- Mobile view works properly on mobile Chrome and Firefox
- Added thin-window view for desktop browsers that have been reduced in size
- Tested in Chrome and Firefox on mobile and desktop

* Improve style for small-width desktop popover
2023-03-23 00:20:34 -07:00
black-sliver
b3895750ab Docs: from source on macOS: update supported python versions 2023-03-22 19:50:30 +01:00
alwaysintreble
7591404151 OOT: set default for adult trade start if the set is empty 2023-03-22 18:39:32 +01:00
JusticePS
d48e1e447f Adventure: implement new game (#1531)
Adds Adventure for the Atari 2600, NTSC version. New randomizer, not based on prior works. Somewhat atypical of current AP rom patch games; The generator does not require the adventure rom, but writes some data to an .apadvn APContainer file that the client uses along with a base bsdiff patch to generate a final rom file.
2023-03-22 15:25:55 +01:00
JaredWeakStrike
206f8cf5ed KH2: fixed bugs of rc1 (#1565)
KH2Client:
- Now checks if the world id is in the list of checks. This fixed sending out stuff on the movie
- Cleaned up unused inports
- Not getting starting invo if the game is not open when you connect to the server

__init__:
-Cleaned up print statements
- Fixed the spoiler log not outputting the right amount of mcguffins after fixing them in the case of the player messing up their settings

Openkh:
-Fixed putting the correct dummy item on levels
2023-03-22 15:21:41 +01:00
Alchav
0c6b1827fe Pokemon R/B: Pokemon Tower Wild Pokemon logic 2023-03-22 15:20:23 +01:00
PoryGone
017f91c1b5 LADX: Remove duplicate Link's Awakening setup (#1573) 2023-03-22 15:19:29 +01:00
Jarno
95b01def6b Timespinner: RC bug, lake serene is dry, when Rising Tides is off (#1570)
* Tinmespinner: RC bug, lake serene is dry, when Rising Tides is off

* yes
2023-03-22 03:32:37 -07:00
black-sliver
5977e401d5 MultiServer: include 'Archipelago' in games, versions and checksums 2023-03-21 23:36:53 +01:00
PoryGone
21a3c74783 SA2B: v2.1 Content Update (#1563)
Changelog:

Features:
- New goal
  - Grand Prix
    - Complete all of the Kart Races to win!
- New optional Location Checks
  - Omosanity (Activating Omochao)
  - Kart Race Mode
- Ring Loss option
  - `Classic` - lose all rings on hit
  - `Modern` - lose 20 rings on hit
  - `OHKO` - instantly die on hit, regardless of ring count (shields still protect you)
- New Trap
  - Pong Trap

Quality of Life:
- SA2B is now distributed as an `.apworld`
- Maximum possible number of Emblems in item pool is increased from 180 to 250
- An indicator now shows on the Stage Select screen when `Cannon's Core` is available
- Certain traps (`Exposition` and `Pong`) are now possible to receive on `Route 101` and `Route 280`
- Certain traps (`Confusion`, `Chaos Control`, `Exposition` and `Pong`) are now possible to receive on `FinalHazard`

Bug Fixes:
- Actually swap Intermediate and Expert Chao Races correctly
- Don't always grant double score for killing Gold Beetles anymore
- Ensure upgrades are applied properly, even when received while dying
- Fix the Message Queue getting disordered when receiving many messages in quick succession
- Fix Logic errors
  - `City Escape - 3` (Hard Logic) now requires no upgrades
  - `Mission Street - Pipe 2` (Hard Logic) now requires no upgrades
  - `Crazy Gadget - Pipe 3` (Hard Logic) now requires no upgrades
  - `Egg Quarters - 3` (Hard Logic) now requires only `Rouge - Mystic Melody`
  - `Mad Space - 5` (Hard Logic) now requires no upgrades

Co-authored-by: RaspberrySpaceJam <tyler.summers@gmail.com>
2023-03-21 21:26:13 +01:00
Zach Parks
2fb9176511 Clique: The greatest game of all time. (#1566) 2023-03-21 21:23:45 +01:00
alwaysintreble
1c69fb3c3c The Messenger: Add more difficult logic options (#1550) 2023-03-21 21:21:27 +01:00
Fabian Dill
91502505a1 WebHost: fix type in states template 2023-03-21 20:05:37 +01:00
black-sliver
01c13ca243 Docs: some clarification in running from source 2023-03-21 18:28:45 +01:00
Fabian Dill
c2a8b842de Core: typo 2023-03-21 15:53:10 +01:00
alwaysintreble
856efebc39 Multiserver: Only update client status for a slot when the first enters and the last leaves (#1358) 2023-03-21 15:50:50 +01:00
Alchav
5a4203649d Pokémon R/B: Quiz fix 2023-03-21 15:46:11 +01:00
Alchav
ddb764a9b6 Pokémon R/B: Skip unnecessary sweeps for events 2023-03-21 15:46:11 +01:00
Rosalie-A
9f65f22fac TLoZ: Installer Info (#1554) 2023-03-21 15:45:31 +01:00
Magnemania
b7ff9b69ba WG: Added Client to Setup (#1556) 2023-03-21 14:18:59 +01:00
Fabian Dill
012e6ba24c WebHost: add Status to MultiTracker 2023-03-21 14:06:38 +01:00
The T
cd9d0bebc8 Add LADX to Readme.md (#1559)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-20 23:55:53 +01:00
Brooty Johnson
3fa6588637 TLoZ: Update instructions (#1558)
Added 'Optional Software' to instructions for setting up Z1
2023-03-20 23:45:36 +01:00
JaredWeakStrike
e6d16c905c KH2: Fixed inno_setup and fixed readme.md (#1557) 2023-03-20 23:43:29 +01:00
Fabian Dill
958829d491 Launcher: dynamic Launcher 2023-03-20 23:40:34 +01:00
KonoTyran
9ee37b0ec5 [Minecraft] Update MinecraftClient.py (#1524)
* add support for direct url for mod to download.
make a nice error message if the chosen release channel is empty.

* Rip out call to github api as it is no longer used.

* rework error message to be more descriptive, and provide basic troubleshooting.

fix command line data version not correctly overriding apmc data version.
2023-03-20 11:44:48 -07:00
zig-for
81a239325d Links Awakening: Implement New Game (#1334)
Adds Link's Awakening: DX. Fully imports and forks LADXR, with permission - https://github.com/daid/LADXR
2023-03-20 17:26:03 +01:00
JaredWeakStrike
67bf12369a KH2 game implementation (#1438) 2023-03-20 17:19:55 +01:00
toasterparty
d4b793902f [OC2] Overworld Logic (#1530) 2023-03-20 17:16:19 +01:00
Fabian Dill
6671b21a86 Core: Generic excluded fill (#1511) 2023-03-20 17:10:12 +01:00
el-u
6d13dc4944 lufia2ac: new features, bug fixes, and more (#1549)
### New features

- ***Architect mode***
  Usually the cave is randomized by the game, meaning that each attempt will produce a different dungeon. However, with this new feature the player can, between runs, opt into keeping the same cave. If activated, they will then encounter the same floor layouts, same enemy spawns, and same red chest contents as on their previous attempt.   

- ***Custom item pool***
  Previously, the multiworld item pool consisted entirely of random blue chest items because, well, the permanent checks are blue chests and that's what one would normally get from these. While blue chest items often greatly increase your odds against regular enemies, being able to defeat the Master can be contingent on having an appropriate equipment setup of red chest items (such as Dekar blade) or even enemy drops (such as Hidora rock), most of which cannot normally be obtained from blue chests.
  With the custom item pool option, players now have the freedom to place any cave item into the multiworld itempool for their world.

- ***Enemy floor number, enemy sprite, and enemy movement pattern randomization***
  Experienced players can deduce a lot of information about the opposition they will be facing, for example: Given the current floor number, one can know in advance which of the enemy types will have a chance to spawn on that floor. And when seeing a particular enemy sprite, one can already know which enemy types one might have to face in battle if one were to come in contact with it, and also how that enemy group will move through the dungeon.
  Three new randomization options are added for players who want to spice up their game: one can shuffle which enemy types appear on which floor, one can shuffle which sprite is used by which enemy type, and one can shuffle which movement pattern is used by which sprite.

- ***EXP modifier***
  Just a simple multiplier option to allow people to level up faster. (For technical reasons, the maximum amount of EXP that can be awarded for a single enemy is limited to 65535, but even with the maximum allowed modifier of 500% there are only 6 enemy types in the cave that can reach this cap.)


### Balance change

- ***proportionally adjust chest type distribution to accommodate increased blue chest chance***
  One of the main problems that became apparent in the current version has to do with the distribution of chest contents. The game considers 6 categories, namely: consumable (mostly non-restorative), consumable (restorative), blue chest item, spell, gear, and weapon. Since only blue chests count as multiworld locations, we want to have a mechanism to customize the blue chest chance.
  Given how the chest types are detetermined in game, a naive implementation of an increased blue chest chance causes only the consumable chance to be decreased in return. In practice, this has resulted in some players of worlds with a high blue chest chance struggling (more than usual) to keep their party alive because they were always low on comsumables that restore HP and MP.
  The new algorithm tries to avoid this one-sided effect by having an increase in blue chest chance resulting in a decrease of all other types, calculated in such a way that the relative distribution of the other 5 categories stays (approximately) the same.


### Bug fixes

- ***prevent using party member items if character is already in party***
  This should have been changed at the same time that 6eb00621e3 was made, but oh well... 

- ***fix glitched sprite when opening a chest immediately after receiving an item***
  When opening a chest right after receiving a multiworld item (such that there were two item get animations in the exact same iteration of the game main loop), the item from the chest would display an incorrect sprite in the wrong place. Fixed by cleaning up some relevant memory addresses after getting the multiworld item.

- ***fix death link***
  There was a condition in `deathlink_kill_player` that looked kinda smart (it checked the time against `last_death_link`), but actually wasn't smart at all because `deathlink_kill_player` is executed as an async task and the main thread will update `last_death_link` after creating the task, meaning that whether or not the incoming death link would actually be passed to the game seems to have been up to a race condition. Fixed by simply removing that check.


### Other

- ***add Lufia II Ancient Cave (and SMW) to the network diagram***
  These two games were missing from the SNES sector.

- ***implement get_filler_item_name***
  Place a restorative consumable instead of a completely random item. (Now the only known problem with item links in lufia2ac is... that noone has ever tested item links. But this should be an improvement at least. Anyway, now #1172 can come ;)
  And btw., if you think that the implementation of random selection in this method looks weird, that's because it is indeed weird. (It tries to recreate the algorithm that the game itself uses when it generates a replacement item for a chest that would contain a spell that the party already knows.)

- ***store all options in a dataclass***
  This is basically like using #993 (but without actual support from core). It makes the lufia2ac world code much nicer to maintain because one doesn't have to change 5 different places anymore when adding or renaming an option.

- ***remove master_hp.scale***
  I have to admit: `scale` was a mistake. Never have I seen a single option value cause so many user misconceptions. Some people assume it affects enemies other than the Master; some people assume it affects stats other than HP; and many people will just assume it is a magic option that will somehow counterbalance whatever settings combination they are currently trying to shoot themselves in the foot with.
  On top of that, the `scale` mechanism probably doesn't provide a good user experience even when used for its intended purpose (since having reached floor XY in general doesn't mean you will have the power to deplete XY% of the Masters usual HP; especially given that, due to the randomness of loot, you are never guaranteed to be able to defeat the vanilla Master even when you have cleared 100% of the floors).
  The intended target audience of the `master_hp` option are people who want to fight the Master (and know how to fight it), but also want to lessen (to a degree of their choosing) the harsh dependence on the specific equipment setups that are usually required to win this fight even when having done all 99 floors. They can achieve this by setting the `master_hp` option to a numeric value appropriate for the level of challenge they are seeking. Therefore, nothing of value should be lost by removing the special `scale` value from the `master_hp` option, while at the same time a major source of user confusion will be eliminated.

- ***typing***
  This (combined with the switch to the option dataclass) greatly reduces the typing problems in the lufia2ac world. The remaining typing errors mostly fall into 4 categories:
  1. Lambdas with defaults (which seem to be incorrectly reported as an error due to a mypy bug)
  1. Classmethods that return instances (which could probably be improved using PEP 673 "Self" types, but that would require Python 3.11 as the minimum supported version)
  1. Everything that inherits from TextChoice (which is a typing mess in core)
  1. Everything related to asar.py (which does not have proper typing and lies outside of this project)

## How was this tested?

https://discord.com/channels/731205301247803413/1080852357442707476 and others
2023-03-20 17:04:57 +01:00
Zach Parks
ff9f563d4a Deprecate data_version and introduce checksum for DataPackages. (#684)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-20 17:01:08 +01:00
Fabian Dill
d825576f12 Factorio: content update
Energy Link:
  * Transfer and Storage increased by 10X
  * Cost of building increased by roughly 10X
  * This is a way to address their effect on performance, as users now need a tenth of them to get the same throughput, it also differentiates them from Accumulators
5 new Traps:
  * Teleport Trap
  * Grenade Trap
  * Cluster Grenade Trap
  * Artillery Trap
  * Atomic Rocket Trap
When max science is lower than min science, the two are now swapped.
Max Evolution Trap count was changed from 25 -> 10.
New option: Ingredients Offset
  * When creating random recipes, use this many more or less ingredients in the new recipe.
2023-03-15 21:38:32 +01:00
Alchav
5d6184f1fd STS: update slot_seeds to per_slot_randoms 2023-03-15 09:10:35 +01:00
alwaysintreble
e433246f0c The Messenger: docs improvement (#1545)
* The Messenger: docs improvement

* more wordy mod link

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* indent

* revert accidental indent

oop

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-14 21:18:55 +01:00
black-sliver
3a190a8fb2 CI: more filters, update CodeQL (#1540)
* CI: fix and more greedy filtering

* CI: only run lint if *.py changed

* CI: only run CodeQL if supported file changed

* CI: fix unittests still triggering for build.yml

* CI: update CodeQL action

* CI: trigger codeql when changing the workflow
2023-03-14 19:29:20 +01:00
Alchav
4b7033fce7 Pokemon R/B: Version 3 final touches (#1542)
* Pokémon R/B: Dexsanity balls

* Pokémon R/B: Early Parcel improvement

* Pokémon R/B: Early Parcel dexsanity stuff only when dexsanity
2023-03-14 18:36:17 +01:00
lordlou
37499b40a1 SMZ3: shop check fix 2 (#1538) 2023-03-14 18:31:51 +01:00
black-sliver
ca2c0e6ce2 CI: update stuff (#1534)
* CI: skip SNI, skip unittests if not needed, run build for setup.py

* CI: update actions

* CI: update upload-artifact

Fixes more warnings
2023-03-14 01:32:00 +01:00
black-sliver
2a28a6de28 Pokemon: apply rename of location_item_name 2023-03-14 01:30:58 +01:00
alwaysintreble
573a1a8402 Core: Add a function to allow worlds to easily allow self-locking items (#1383)
* implement function to allow self locking items for items accessibility

* swap some lttp locations to use new functionality

* lambda capture `item_name` and `location`

* don't lambda capture location

* Revert weird visual indent

* make location.always_allow additive

* fix always_allow rule for multiple items

* don't need to lambda capture item_names

* oop

* move player assignment to the beginning

* always_allow should only be for that player so prevent non_local_items

* messenger got merged so have it use this

* Core: fix doc string indentation for allow_self_locking_items

* Core: fix doc string indentation for allow_self_locking_items, number two

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-14 00:55:34 +01:00
Jarno
060ee926e7 Core: type specified missing per_game_common_options (#1509) 2023-03-13 23:45:56 +01:00
Alchav
df55455fc0 Pokémon R/B: Version 3 (#1520)
* Coin items received or found in the Game Corner are now shuffled, locations require Coin Case
* Prizesanity option (shuffle Game Corner Prizes)
* DexSanity option: location checks for marking Pokémon as caught in your Pokédex. Also an option to set all Pokémon in your Pokédex as seen from the start, to aid in locating them.
* Option to randomize the layout of the Rock Tunnel.
* Area 1-to-1 mapping: When one instance of a Wild Pokémon in a given area is randomized, all instances of that Pokémon will be the same. So that if a route had 3 different Pokémon before, it will have 3 after randomization.
* Option to randomize the moves taught by TMs.
* Exact controls for TM/HM compatibility chances.
* Option to randomize Pokémon's pallets or set them based on primary type.
* Added Cinnabar Gym trainers to Trainersanity and randomized the quiz questions and answers. Getting a correct answer will flag the trainer as defeated so that you can obtain the Trainersanity check without defeating the trainer if you answer correctly.
2023-03-13 23:40:55 +01:00
Fabian Dill
4d7bd929bc WebHost: update modules 2023-03-13 21:34:24 +01:00
Fabian Dill
030e41363a Setup: update cx-Freeze 2023-03-13 21:34:07 +01:00
Qwazzy
4bc0e84a7f Docs: SM64 Guide update to explain how to launch the game with batch files (#768)
* Update setup_en.md

Added several sections in regards to opening the completed SM64 build with batch files instead of SM64PCLauncher.

* Update setup_en.md

* Update setup_en.md

* Update setup_en.md

* Update setup_en.md

* Apply suggestions from code review

Co-authored-by: Yussur Mustafa Oraji <N00byKing@hotmail.de>
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* Apply more suggestions from code review

matches the original suggestion from SoldierofOrder

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Yussur Mustafa Oraji <N00byKing@hotmail.de>
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
2023-03-13 00:58:17 +01:00
alwaysintreble
070a92e76c The Messenger: implement new game (#1494)
* initial commit of messenger integration

* setup no_logic and needed slot_data

* fix some typos and determinism

* make all of it deterministic

* add documentation

* swapped to non local items so change the fed data

* ~~deathlink~~

* satisfy the docs test

* update doc test to show expected name

* split custom classes into a separate file and fix an errant rule

* make access dependency test give more useful errors

* implement tests

* remove some unneccessary back entrances and make names clearer

* fix some big dumbs

* successful unit tests are good also some slight reorganizing

* add astral tea quest line, and potentially power seals as items

* if TYPE_CHECKING... aahhhhhh

* oop forgot to remove legacy code

* having the seed and leaves as actual items doesn't seem to do anything so remove them. locations still work though

* update setup guide with some changes

* Tower HQ was creating duplicate locations

* allow self locking items

* cleanup

* move self_locking_items function to core

* docstring

* implement choice of notes needed for music box

* test the default value

* don't create any starting inventory items

* make item creation faster

* change default accessibility and power seals options

* improve documentation

* precollected_items is a dict of Items...

* implement shop chest goal

* tests

* always assign total and required seals

* add new goals and set music box as requiring shop chest on shop chest goals instead of just setting it as the completion

* fix dumb test quirk

* implement music box skip as an option

* world rewrite/cleanup

* default to apworld and add game to readme

* revert bleeding commits from other PRs

* more bleeds

* fix some errors in options docstrings

* ???

* make my set rules method not have an awful name

* test cleanup

* add a test for item accessibility

* fix issues with tests

* make the self locking item behavior work correctly

* misc cleanup

* more general cleanup to be a good example

* quick rules rewrite

* more general cleanup and typing

* more speed, more clean

* bump data version

* make sure the locked item belongs to current player

* fix bad name and indent. call MessengerItem directly for events

* add poptracker pack to docs

* doc cleanup and "known issues" section that I probably won't be able to fix any time soon.

* missed some spots

* add another bug i forgot about

* be consistently wrong
2023-03-12 15:05:50 +01:00
Fabian Dill
39563cc347 WebHost: add game and Factorio to multitracker (#1526)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-12 12:38:13 +01:00
alwaysintreble
54cce4c392 LTTP: fix ice rod hunt boss shuffle (#1529) 2023-03-12 11:03:48 +01:00
el-u
426a81a065 oot/alttp: fix bugs found through MMBN3 testing (#1527) 2023-03-11 20:15:30 +01:00
alwaysintreble
04e6a8eae8 FFR: add option __doc__s 2023-03-11 13:38:27 +01:00
NewSoupVi
0cfdc973f6 The Witness - Expert logic bug (could lead to broken seeds) (#1525) 2023-03-11 10:09:09 +01:00
alwaysintreble
f3ca0a21c9 Docs: Add an option api doc (#1181)
* write up an option api doc

* address reviews

* some clarification

* add note about using schema

* Add ItemSet and formatting

* bulletpoint option defining

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* split random description to new sentence

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* use inclusive and parallel language for example

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* changes from review

* commas

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* capitalize Toggle

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* the sliver conventions

---------

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
2023-03-11 01:14:44 +01:00
Chris Wilson
5fef41eb97 [WebHost] Make site header mobile-friendly (#1523) 2023-03-10 17:21:17 -05:00
alwaysintreble
4068ba2f15 LTTP: add option __doc__s (#1521)
* LTTP: add option `__doc__`s

* review comments
2023-03-10 07:59:47 +01:00
NewSoupVi
b1599c557f The Witness: Death Link + Small bug fixes (#1515)
* Fully functional DeathLink implementation. But it's always on right now :D

* Death Link options. Last commit: All entity names being sent through slot_data

* Tutorial Gate Close logic fix

* Improved option tooltip wording

* Fixed shuffle_postgame: false not excluding some locations

* Link to latest stable client rather than full releases page
2023-03-10 07:58:00 +01:00
Fabian Dill
7fdf38b2ad WebHost: automatically fill PATCH_TARGET -> HOST_ADDRESS and re-use it for rooms (#1518) 2023-03-09 21:31:00 +01:00
Freya Arbjerg
2e76085cf1 WebHost: Fix generic tracker Datatables (#1519) 2023-03-09 19:24:38 +01:00
Fabian Dill
c61f467218 WebHost: fix location_name_group related spinup crash 2023-03-09 12:31:35 +01:00
KonoTyran
942d689093 [Slay the Spire] Enable support for modded characters, and add downfall support (#1368)
* add ability to choose custom characters in STS

* bump required protocol (client?) version.

* fix slot data fill.

* add downfall mode, as well as characters.

* small change in documentation for character choice as it now uses internal ID's instead of visible titles... because other languages are a thing.
2023-03-08 20:14:54 -08:00
espeon65536
5e1aa52373 Minecraft rewrite (#1493)
* Minecraft: rewrite to modern AP standards

* Fix gitignore to not try to ignore the entire minecraft world

* minecraft: clean up MC-specific tests

* minecraft: use pkgutil instead of open

* minecraft: ship as apworld

* mc: update region to new api

* Increase upper limit on advancement and egg shard goals

* mc: reduce egg shard count by 5 for structure compasses

* Minecraft: add more tests
Ensures data loading works; tests beatability with various options at their max setting; new tests for 1.19 advancements

* test improvements

* mc: typing and imports cleanup

* parens

* mc: condense filler item code and override get_filler_item_name
2023-03-08 20:13:52 -08:00
Freya Arbjerg
a95e51deda Add generic multiworld tracker, move lttp multiworld tracker (#1478)
Co-authored-by: Berserker
2023-03-08 22:39:15 +01:00
alwaysintreble
738319462d Spoiler: Don't double print if world overrides common options (#1505) 2023-03-08 22:19:38 +01:00
alwaysintreble
e3deb822ad Core: implement location_name_groups (#1502) 2023-03-08 22:15:28 +01:00
Jarno
d57314a407 Timespinner: Bring back starter progression item (#1508) 2023-03-08 20:05:30 +01:00
t3hf1gm3nt
5a8e6e61f5 TLOZ: Code Cleanup (#1514)
- consolidated declaration and population of level location lists
- moved floor_location_game_ids_late declaration for consistency
- moved generate_itempool to create_items, where it belongs
- mention that expanded pool includes take any caves in the option description again
- removed unnecessary StartingPosition check regarding Take Any Caves (leftover from older StartingPosition behavior I believe)
- use proper comparisons to option keys instead of hardcoded ints
2023-03-08 11:22:14 +01:00
Magnemania
17e90ce12c SC2: Greater variety on short generations (#1367)
Originally, short generations used an artificial cull to create balanced mission distributions. This resulted in campaigns that were somewhat too consistent, and on some standard settings combinations, this resulted in campaigns having The Outlaws as the second mission 100% of the time. It also caused generation to fail a bit too easily if the player excluded too many missions.

This removes the cull and adds an additional early Easy mission slot to all of the reduced sized campaigns.

When playing on No Build settings, this also pushes many of the missions down a difficulty level to ensure greater variety, and pushes additional missions down on Advanced Tactics.

Additional small fixes:

The in-world Excluded Missions validation check is replaced by the core OptionSet check.
Fixed issue with Existing Items not getting their upgrades locked with Units Always Have Upgrades on.
2023-03-07 14:14:49 +01:00
NewSoupVi
016157a0eb Witness: Fixed settings combination not rolling (see description)
Settings combination:
- EP Shuffle
- disable_non_randomized
- doors: panels or doors: none

An Event Item was being created that is inaccessible. This is fixed now.
(The fix makes sure that player_logic is not trying to create events for the sake of EPs that are disabled)

Note: These two sets should probably be merged anyway, they used to behave differenty but no longer really do. But that will require some extra care on the client side as well.
2023-03-07 09:13:54 +01:00
Fabian Dill
5b64c5f934 Subnautica: fix exported radiation logic (#1507) 2023-03-07 09:09:24 +01:00
alwaysintreble
414166f6a2 Core: Minor Options cleanup (#1182)
* Options.py cleanup

* TextChoice cleanup

* make Option.current_option_name a property

* title the TextChoce option names

* satisfy the linter

* a little more options cleanup

* move the typing import

* typing should be PlandoSettings

* fix incorrect conflict merging

* make imports local

* the tests seem to want me to import these twice though i hate it.

* changes from review. Make the various Location verifying Options `LocationSet`

* remove unnecessary fluff

* begrudgingly support get_current_option_name. Leave a comment that worlds shouldn't be touching this

* log a deprecation warning and return the property for `get_current_option_name()`

---------

Co-authored-by: beauxq <beauxq@yahoo.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-07 08:44:20 +01:00
beauxq
e6109394ad Zillion: use Option.current_key
and other minor fixes
2023-03-07 08:33:33 +01:00
Fabian Dill
8ca25fed63 Setup: clean up imports 2023-03-06 12:54:32 +01:00
0rganics
227d59ecfb WebHost: Add a ChecksFinder tracker (#1333)
Co-authored-by: Chris Wilson <chris@legendserver.info>
2023-03-05 14:17:04 +01:00
Fabian Dill
08c17c83d4 Setup: auto download SNI (#1312)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-05 14:10:05 +01:00
Rosalie-A
efb2ab4505 TLoZ: Implementing The Legend of Zelda (#1354)
Co-authored-by: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com>
Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com>
2023-03-05 13:31:31 +01:00
Joethepic
3a68ce3faa PKMN: Make Exp All early (#1422) 2023-03-05 10:08:32 +01:00
black-sliver
e78800d1bc Item Plando: make world selection deterministic 2023-03-05 02:16:55 +01:00
Jérémie Bolduc
96d7a3a64c Stardew Valley: Fix generation issue with Master Angler goal and vanilla tools (#1498)
* - Can Catch every fish doesn't need fishing rods if they are not shuffled

* add has_max_fishing_rod

* add test for master angler + vanilla tools

---------

Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-03-04 18:34:51 +01:00
recklesscoder
30b70b2055 Misc collected fixes (#1497) 2023-03-04 16:34:10 +01:00
Jarno
cd234fc04a Timespinner: Fixed Dry lake serene oddity (#1501) 2023-03-04 16:31:44 +01:00
zig-for
d74c4c4c94 Core: Remove ALTTP cruft from BaseClasses (#1451) 2023-03-04 08:23:52 +01:00
Jarno
a4b61118cf Timespinner: Refactorings + fix for #1460 (#1484) 2023-03-04 08:16:05 +01:00
FlySniper
9fa1f4e85f Wargroove: Fixed Wargroove Client not removing communication files (#1492) 2023-03-03 18:24:09 +01:00
black-sliver
3a926849a0 CI: run unittests on macos
this is to ensure dependencies can be installed and loaded on macos (on AMD64)
2023-03-03 18:22:31 +01:00
Alchav
798d823397 Core: Check for show_in_spoiler (#1500) 2023-03-03 17:22:46 +01:00
Fabian Dill
4ea582f14e Windows: allow arm64 setup (#1496) 2023-03-03 09:51:36 +01:00
alwaysintreble
21fb16291d Tests: test that the game is beatable for WorldTestBases (#1495)
* Tests: test that the game is beatable for WorldTestBases

* update docstring

* don't test the bases with default options for real this time

* invert the property so worlds can use it easier

* setup check should be or

* test class needs to always be constructed

* skip default tests before multiworld setup

* check if the calling method is in the base's __dict__

* shorter property and functional setup skipping

* shorter property and functional setup skipping
2023-03-03 00:30:40 +01:00
NewSoupVi
805f33c39e Witness: Bugfixes in response to beta tests (#1473)
* Make all Keep Pressure Plates logically required for the Laser Panel

* Added more Tutorial checks

* Added the remaining two Shipwreck Boat EPs to the exclude list for normal

* Improved itempool filling system, added warning if usefuls had to be eaten

* Moved creation of said warning string to utils

* Fixed logic bug causing broken seeds on Mountain Floor 2

* Hints system change

* Expert Logic Fix

* Fixed typo

* Better wording

* Added missing games to junk hints

* Made sure Entrance names are unique

* Fixed missing Obelisk Side

* Disable Non Randomized + EP Shuffle fix

* Fixed disable_non_randomized precompleted EPs being 'disabled' instead of 'precompleted'

* Fixed if/elif error

* Tutorial Gate Open local symbol item becomes local_early_item in expert instead

* Bump required client version. There is a beta client that sends 0.3.9.

* Removed print statement, oops

* Fixed itempool manipulation in pre_fill

* Replaced string concats with fstrings

* Improved make_warning_string function signature

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Improved performance on removing multiple items from multiworld itempool

* Comment

* Fixed errors with the code

* Made removal from itempool not fail unit test for multiple references

* Moved all item creation to create_items, got rid of itempool modifying system

* Colored Squares is no longer a good item, that's outdated

* Removed double if

* React to from_pool: false by removing a junk item

* Fixed warning if only Fnc Brain was removed

* Make use of string truthiness instead

* Made reading of plandoed items safer

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-03 00:08:24 +01:00
el-u
0cf8206660 launcher: add .apl2ac support 2023-03-01 22:56:14 +01:00
Fabian Dill
2c20b56478 Core: count the world types 2023-03-01 05:47:46 +01:00
FlySniper
1d2f7d8669 Wargroove: Fixed the find all dogs check activating prematurely (#1486) 2023-02-28 16:26:48 +01:00
Fabian Dill
0733775f2c Subnautica: Allow either utility room for progression 2023-02-28 11:22:47 +01:00
black-sliver
d6f3b27695 DKC3, SMW: use user_path for file
Same as for other games, this will resolve to ~/Archipelago on Linux, if the install folder is read-only
2023-02-28 09:51:32 +01:00
black-sliver
ce7e6bcf33 Readme: fix order 2023-02-28 09:15:23 +01:00
Jarno
2c4658a7e0 Docs: More games more fun 2023-02-27 23:19:33 +01:00
TheLynk
79b8733b13 OoT, MC: add new translation setup in french (#1410)
* add new translation

* Add translation for OOT Setup in french

* Update setup_fr.md

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update setup_fr.md

Fix treu to true

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Marech <marechal-l@gmx.com>

* Update OOT Init and Update Minecraft Init

* Fix formatting errors

---------

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-02-27 23:17:54 +01:00
alwaysintreble
9cb9cbe47d Tests: test that worlds don't create regions or locations after create_items (#1465)
* Tests: test that worlds don't create regions or locations after `create_items`

* recache during the location counts just to be extra safe

* adjust typing and use a Tuple instead of a list

* remove unused import
2023-02-27 02:13:24 +01:00
alwaysintreble
7cad53c31a Docs: add docstrings to the World class 2023-02-27 01:39:30 +01:00
alwaysintreble
f3bdf0c5ed Tests: test all state and empty state on world test bases (#1476)
* Tests: test all state and empty state on world test bases

* actually add the test methods to the dict

* only test if the world test base has non default options

* remove temp logging

* ditch the meta class and document methods

* Tests: WorldTestBase comment and docstring cleanup

* skip default tests if setUp or world_setup are modified and use a property

* negation hurts my head

* docstring

* use a better name for the property

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-02-27 01:24:54 +01:00
Jérémie Bolduc
af7d0dbf37 Stardew Valley: implement new game (#1455)
* Stardew Valley Archipelago implementation

* fix breaking changes

* - Added and Updated Documentation for the game

* Removed fun

* Remove entire idea of step, due to possible inconsistency with the main AP core

* Commented out the desired steps, fix renaming after rebase

* Fixed wording

* tests now passes on 3.8

* run flake8

* remove dependency so apworld work again

* remove dependency for real

* - Fix Formatting in the Game Page
- Removed disabled Option Descriptions for Entrance Randomizer
- Improved Game Page's description of the Arcade Machine buffs
- Trimmed down the text on the Options page for Arcade Machines, so that it is smaller

* - Removed blankspace

* remove player field

* remove None check in options

* document the scripts

* fix pytest warning

* use importlib.resources.files

* fix

* add version requirement to importlib_resources

* remove __init__.py from data folder

* increment data version

* let the __init__.py for 3.9

* use sorted() instead of list()

* replace frozenset from fish_data with tuples

* remove dependency on pytest

* - Add a bit of text to the guide to tell them about how to redeem some received items

* - Added a comment about which mod version to use

* change single quotes for double quotes

* Minimum client version both ways

* Changed version number to be more specific. The mod will handle deciding

---------

Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-02-27 01:19:15 +01:00
Fabian Dill
0286edf20c Subnautica: fix the test that I didn't mean to push yet 2023-02-26 09:51:02 +01:00
Fabian Dill
05e36cab1c Subnautica: increment version 2023-02-26 09:48:51 +01:00
Klenoa
50425985c4 Subnautica: Rename location check (id 33029) (#1120)
Update southwest grassy plateaus wreck to 'Grassy Plateaus Southwest Wreck - Databox' instead of 'Grassy Plateaus West Wreck - Databox'
2023-02-26 09:48:08 +01:00
Marech
062d6eeace DS3: Added DLC Items/Locations + corresponding option and added an option to enable materials/consumables/estus randomization (#1301)
- Added more progressive locations and associated items.
- Added an option to enable materials/consumables/estus randomization, some players complain about the number of locations and the randomness of those items.
- Added an option to add DLC Items and Locations to the pool, the player must own both the ASHES OF ARIANDEL and the RINGED CITY DLC.

Co-authored-by: Br00ty <83629348+Br00ty@users.noreply.github.com>
Co-authored-by: Friðberg Reynir Traustason <fridberg.traustason@gmail.com>
2023-02-26 06:35:03 +01:00
alwaysintreble
6c460bcbf7 LTTP: Move LTTP spoiler writing out of core (#1467) 2023-02-25 04:02:51 +01:00
toasterparty
b8659d28cc [OC2] DeathLink (#1470) 2023-02-24 08:32:15 +01:00
alwaysintreble
0b12d80008 Tracker: get game names from slot_info instead of multidata["games"] and render custom game names on generic tracker (#1453) 2023-02-24 08:30:11 +01:00
FlySniper
5966aa5327 Wargroove: Implement New Game (#1401)
This adds Wargroove to the list of supported games. Wargroove uses a custom non-linear campaign over the vanilla and double trouble campaigns. A Wargroove client has been added which does a lot of heavy lifting for the Wargroove implementation and must be always on during gameplay. The mod source files can be found here: https://github.com/FlySniper/WargrooveArchipelagoMod
2023-02-24 07:35:09 +01:00
Trevor L
7c68e91d4a Blasphemous: Implement new game (#1446)
Adds @BrandenEK's Blasphemous Randomizer as a new Archipelago game.
2023-02-24 07:33:09 +01:00
alwaysintreble
1d6ab13015 ArchipIDLE: add a completion condition instead of hard coding tests around a game (#1444) 2023-02-23 21:16:10 -05:00
CaitSith2
cb3d40624c Timespinner: Make RisingTidesOverrides consistent with normal yaml behaviour. (#1474)
* Make RisingTidesOverrides consistent with normal yaml behaviour.

* Each of the options can be either string directly specifying the option, or dictionary.
* If dictionary, ensure that at least one of the options is greater than zero.

* Made keys optional

* A lot less copy/pasta.

---------

Co-authored-by: Jarno Westhof <jarnowesthof@gmail.com>
2023-02-22 17:11:27 -08:00
alwaysintreble
0eb66957b1 SMW: change random in generate_output to use slot random 2023-02-20 18:43:23 +01:00
alwaysintreble
53e2232f29 Docs: document world docs and tests (#1463)
* Docs: document world docs and tests

* regions and items shouldn't be created after `create_items`

* Changes from review

* Restructure game info section

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* w

* urls can have extension probably

* reorder the methods by call order

* fix grammar mistake in ordered method list

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-02-19 23:16:56 +01:00
Fabian Dill
ecd2675ea8 Tests: check that Regions are reachable (#1034)
* Tests: check that Regions are reachable
try to prevent errors from unconnected/never reachable Regions

* Test region access (#1039)

* Tests: note oot's default unreachable regions

* [SM] Fixed failing testAllStateCanReachEverything (#1087)

* [SM] Fixed failing testAllStateCanReachEverything

- by adding exclusion for Regions used only when corresponding Starting Location is used
- by removing unnecessary VARIA Regions used only for EscapeRando (not supported in AP anyway)

* Update worlds/sm/Regions.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update worlds/sm/Rules.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update worlds/sm/Regions.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update test/general/TestReachability.py

---------

Co-authored-by: espeon65536 <81029175+espeon65536@users.noreply.github.com>
Co-authored-by: lordlou <87331798+lordlou@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-02-19 23:09:54 +01:00
Jarno
fc2e555b4a Timespinner: many new stuffs (#1433)
* Timespinner: added RisingTides and DadPercent flags

* Implemented logic for DadPercent and RisingTides

* Fixed TODO's

* Logic fixes

* Fixed + removed LogicMixins

* Fixes

* More Fixes

* Added UnchainedKeys flag

* Fixed available items in pool with UnchainedKeys

* Fixed typing callable

* Fixed generation failures

* More refactorings

* Implemented traps

* Fixed more typo

* Fixed copy paste bug

* Fixed teleporter logic

* Fixed traps from pool

* Fixed pyramid gates bug that causes a crash on connecting

* Fixed seed reproduceability

* Fixed logic eye for eye spy
Now consider warp beacons as starter progression items

* Attempt to add tracker icons using table

* Replaced table layout with css grid

* Fixed tracker + added Timespinner was apworld capatible

* Updated archipelago items description

* updated URL

* Cleared up text

* Fixed based on self review of PR

* Fixed unit tests

* Fixed seed reproduceability when the traps yaml option is not provided

* Fixed logic for flooded basement

* Implemented Beserkers review result

I am not sure why, i guess this is just to make adding future games less conflicting?

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>

* Added two new options (thanks to WeffJebster)

* Apply suggestions from code review

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Addition review results

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-02-19 21:22:30 +01:00
black-sliver
df020bb389 Style Guide: add world consistency 2023-02-19 19:34:45 +01:00
PoryGone
7760034ff7 DKC3 and SMW: Remove relative imports (#1472) 2023-02-19 09:10:32 +01:00
black-sliver
3e7794d5dc SoE: update evermizer to 044
* Fix energy core despawning when looting failed
* Fix fish guy dialog when cure was already obtained
see https://github.com/black-sliver/pyevermizer/releases/tag/v0.44.0 for more details
2023-02-18 15:18:51 +01:00
black-sliver
a3e8bb474a ModuleUpdater: allow new syntax, nicer output 2023-02-18 15:18:51 +01:00
kindasneaki
e4c95c940a RoR2: regions unreachable fix (#1459) 2023-02-17 22:08:18 +01:00
recklesscoder
daa1809a0f WebHost: Tweaks to search on tracker pages (#1307)
* WebHost: Tweaks to search on tracker pages
- Pressing `Ctrl+F` or `/` now focuses the search box.
- Typing now automatically focuses the search box.
- Pressing `Escape` now clears the search and scrolls to the top.

* WebHost/Trackers: Focus search box on load

* WebHost/Trackers: Remove overriding of Ctrl+F and /
2023-02-17 13:24:21 -05:00
Jarno
0a1261eb84 WebHost: Add tutorials to sitemap and hide settings link for games without settings (#1452)
* WebHost: Add tutorials to sitemap and hide settings link for games without settings

* Fixed some typing imports
2023-02-17 13:16:37 -05:00
toasterparty
b62be6f7f4 [OC2] Colored Ramp Button Items (#1466)
Before: 1 item activates all 4
After: 7 items activate 7 buttons, creating more divergent routes

Also, I consolidated the 6 filler emotes into a single "Emote Wheel" item to make space in the item pool.

I bumped my data version and min AP version to indicate this change.

The corresponding oc2-modding update is **v1.6.0**
2023-02-17 09:25:56 +01:00
toasterparty
ce2553a2b3 [OC2] Location Balancing (#1458) 2023-02-17 09:21:56 +01:00
Fabian Dill
18c4b4b1fe Subnautica: move code to be a better example 2023-02-17 08:50:22 +01:00
Fabian Dill
a85ca9cc87 Subnautica: add logic dumper for mod (#1386)
* Subnautica: add logic dumper for mod

* Subnautica: export more data

* Subnautica: fix some Cyclops logic
2023-02-16 00:40:19 +01:00
el-u
ad4846cedd core: clarify usage of classmethods in World class (#1449) 2023-02-16 00:28:02 +01:00
recklesscoder
b20be3ccec Docs/Factorio: Document EnergyLink (#1456)
* Docs/Factorio: Document EnergyLink

* Docs/Factorio: EnergyLink clarification
2023-02-16 00:25:46 +01:00
alwaysintreble
8af7908cd0 Tests: datapackage and more multiworld renaming (#1454)
* Tests: add a test that created items and locations exist in the datapackage

* move FF validation to `assert_generate` and remove test exclusion

* test created location addresses are correct

* make the assertion proper and more verbose

* make item count test ~~a bit faster~~ a lot nicer

* 120 blaze it

* name test multiworld setup better and fix another over 120 line in FFR
2023-02-15 22:46:10 +01:00
alwaysintreble
f078750b72 LTTP: make the enemizer check a property and only check for it once instead of per world (#1448)
* LTTP: do the enemizer check in `stage_assert_generate` and break after checking any world for enemizer succeeds

* use multiworld

* catch a missed `used_enemizer` check and add typing

* more typing
2023-02-14 22:22:39 +01:00
alwaysintreble
7cbeb8438b core: rip out RegionType and rework Region class (#814) 2023-02-14 01:06:43 +01:00
Fabian Dill
f7a0542898 kvui: limit UI side logs to by default 1000 messages 2023-02-13 09:02:19 +01:00
recklesscoder
cc61f16e57 Protocol: Improve machine-readability of prints (#1388)
* Protocol: Improve machine-readability of prints

* Factorio: Make use of new PrintJSON fields for echo detection.

* Protocol: Add message field to chat prints.
2023-02-13 03:17:25 +01:00
alwaysintreble
9e3c2e2464 Tests: test that exits to Regions are the parents of the Entrance (#1442) 2023-02-13 02:05:52 +01:00
Fabian Dill
f528175d8a Core: prepare server for removal of names in multidata (#1430) 2023-02-13 01:56:20 +01:00
Fabian Dill
803d7105a1 kivy: allow user-defined text colors in data/client.kv (#1429) 2023-02-13 01:55:43 +01:00
el-u
a40f6058b5 lufia2ac: fix mismatched exits/parent region 2023-02-13 00:46:46 +01:00
toasterparty
0ff3c693d5 [OC2] Relax Horde Logic for Horde H-8 and Winter H-4 (#1439) 2023-02-10 21:42:28 +01:00
Fabian Dill
873a374a69 SNIclient: connect fixes (#1436) 2023-02-07 10:16:39 +01:00
black-sliver
60584b7617 CI: more pip to fix the build 2023-02-07 10:10:27 +01:00
black-sliver
e24a85ca5c CI: update SNI to v0.0.88 2023-02-07 10:10:27 +01:00
lordlou
cc0540d3fb SMZ3: keysanity accessibility fix (#1428) 2023-02-07 03:14:03 +01:00
NewSoupVi
c360b9266c Witness: Renaming: Mill -> Stoneworks, correcting order for 'First, Second, ...', removed all instances of 'Door (Door)' (#1435) 2023-02-07 03:12:47 +01:00
beauxq
6148213e43 Zillion: fix name data overflow 2023-02-07 03:11:48 +01:00
Jarno
ff175008a1 Core: Phase out Print packets (#1364) 2023-02-05 22:06:38 +01:00
kindasneaki
cae1e683e2 RoR2: 1.20 content update (#1396)
## Adding in Explore Mode:

Features include:
* Added in `environments` to be items.
* `Location checks` are now `environment based` instead of being able to get them from anywhere.
* Added in support for the `DLC Survivors of the void` which include `Void Items` and `3 new maps` that come with it. (option added to use DLC)

---------

Co-authored-by: Dogpetkid <dogpetkid@gmail.com>
2023-02-05 21:51:03 +01:00
vgZerst
fb1a9e9c5a WebHost: add checks percent done column to tracker (#1376)
* WebHost: add checks percent done column to tracker

* WebHost: add checks percent done column to tracker
2023-02-04 00:04:00 -05:00
CaitSith2
555a0da46d Core: Rename the missed slot_seeds. (#1432)
* Rename the missed slot_seeds.

* Fixed a threaded context random error.
2023-02-03 19:39:18 +01:00
NewSoupVi
0817305d5b Witness: Added an option tooltip for "Environmental Puzzles Difficulty" option (+ another bugfix) (#1431)
* Added an option tooltip

* Fixed eclipse being on in EP difficulty normal
2023-02-03 19:38:54 +01:00
Fabian Dill
995c978628 Core: replace global random state with descriptive error (#1424)
* Core: replace global random state with descriptive error

* Core: make random a proxy object and rename slot_seeds
2023-02-02 01:14:23 +01:00
NewSoupVi
4de7ebd8b0 The Witness: v4 Content Update (#1338)
## New Features:

- EP Shuffle (Individual or Obelisk Sides, with varying difficulty levels)
- Ability to play without Puzzle Randomization (I.e. vanilla + AP layer)
- Pet the Dog to get a Puzzle Skip :) (No, really.)

## Changes:

- Starting inventory behavior improved (Consider starting items like doors and lasers logically even if they aren't part of the mode)
- Audio Log hint system improved (On low hint counts, you will no longer get the same locations hinted every time, i.e. always hints are shuffled)

## Fixes:

- Many fixes to symbol requirements
- Fixes to "shuffle_postgame" (What checks are evaluated as "postgame" in specific modes)
- Logically irrelevant doors are now "useful" instead of "progression"
2023-02-01 21:18:07 +01:00
PoryGone
3cef39a387 SMW & DKC3: Ship as .apworld (#1426) 2023-02-01 21:15:01 +01:00
el-u
ffff9ece55 core: properly declare from_any as an abstract classmethod 2023-02-01 21:14:11 +01:00
PoryGone
dc2aa5f41e SMW: v1.1 Content Update (#1344)
* Make Bowser unkillable on Egg Hunt

* Increment Data Package version

Changed a location name.

* Baseline for Bowser Rooms shuffling

* Add boss shuffle

* Remove extra space

* Overworld Palette Shuffle

* Fix Literature Trap typo

* Handle Queuing traps and new Timer Trap

* Fix trap name and actually create them

* Early Climb and Overworld Speed

* Add correct tooltip for Early Climb

* Tooltip text edit

* Address unconnected regions

* Add option to fully exclude Special Zone levels from the seed

* Fix Chocolate Island 4 Dragon Coins logic

* Update worlds/smw/Client.py to use `getattr`
2023-01-30 05:53:56 +01:00
black-sliver
428344b6bc setup: honor build automation ...
... and reorder imports to PEP it up
2023-01-30 02:33:41 +01:00
Fabian Dill
ea2175cb8a MultiServer: load old forfeit_mode if release_mode not present 2023-01-30 00:54:57 +01:00
Fabian Dill
11873e059a Setup: don't use dependency before it's installed 2023-01-29 22:36:04 +01:00
Fabian Dill
6c1023a88c Subnautica: fix swim_rule considers items property use (#1419) 2023-01-29 22:12:39 +01:00
The T
0be0732a2b WebHost: FAQ: change "seeds" to "a world" where world is the right term.
Berserker has frequently corrected, that each player's game is a single world, inside a larger seed; not that each player's game is a seed.
2023-01-29 22:11:53 +01:00
lordlou
c9aa283711 SMZ3: chest game fix (#1417)
Fixed DW Chest Game always sending checks for the 2 chests. The checks sent were the proper "Chest Game" location for the first time the player would open the second chest but all other times, it would send either the last check that was done or default to sending location 0x00 which is SM "Power Bomb (Crateria surface)".
2023-01-28 01:51:19 +01:00
Fabian Dill
cf2204a861 Factorio: add option "Tech Cost Distribution" (#1404)
* Factorio: add option "Tech Cost Distribution"

* TextClient: None out game on disconnect

* TextClient: disconnect is async
2023-01-27 15:38:12 -08:00
Fabian Dill
dfdcad28e5 TextcVient: game reset (#1416)
* TextClient: None out game on disconnect
2023-01-28 00:32:48 +01:00
Fabian Dill
ab4324c901 Factorio: add option "ramping tech cost" (#1403)
* Factorio: add option "ramping tech cost"

* Factorio: fix missing s

* Factorio: add display_name to ranmping tech costs
2023-01-27 15:30:05 -08:00
Fabian Dill
1e251dcdc0 Setup: new cx-Freeze just dropped 2023-01-27 20:06:56 +01:00
espeon65536
9c1f7bfea9 oot: remove special NL exceptions in entrance randomization
turns out they were causing lots of issues
2023-01-26 21:24:27 +01:00
KonoTyran
5393563700 MultiServer: Data Storage Additions #1411
adds 3 new operations to datastorage that allows adding and removing of elements from list and dicts.
2023-01-25 06:14:46 +01:00
toasterparty
28576f2b0d OC2: decrease default difficulty (#1413) 2023-01-25 01:04:13 +01:00
Fabian Dill
ba519fecd0 Setup: update some stuff to 6.14.0 cx-Freeze (#1412)
* Setup: update some stuff to 6.14.0 cx-Freeze

* Fix BuildCommand and replace include_files by cutom step

* setup.py: bit more cleanup for extra_libs

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-01-25 00:20:26 +01:00
espeon65536
86fb450ecc Core: recache all locations before locality rules
Some worlds would not trigger a recache, causing locations to be missed when setting locality rules.
2023-01-24 06:14:31 +01:00
SonicRPika
920240cb6f Pokemon Red and Blue: Fix to having all traps disabled (#1408) 2023-01-24 04:34:45 +01:00
Fabian Dill
53dd0d5a7d SC2: verify downloaded data is a zipfile 2023-01-24 03:54:23 +01:00
Fabian Dill
807f544b26 SC2: use warning log level for potentially broken map files 2023-01-24 03:45:43 +01:00
SoldierofOrder
1d1693df62 SC2Client: Changes to /download_data and feedback (#1347)
* SC2Client: Added feedback for users who have map files, but no version #.

* SC2Client: Fixed a missing space.

* SC2Client: /download_data now always forces a download.
2023-01-24 03:44:12 +01:00
Fabian Dill
51574959ec Setup: add moduleupdater prompt to setup.py 2023-01-24 03:42:55 +01:00
Fabian Dill
04f726aef2 kvui: always display all tab headers (#1399) 2023-01-24 03:42:34 +01:00
CaitSith2
8a4298e504 ALttP: Fix hint tile hints being potentially useless with item links. (#1400)
* ALttP: Fix hint tile hints being potentially useless with item links.

* use the set returned from world.get_player_groups(player)

* Move the group resolving to BaseClasses. Fix silver arrow hints as well.
2023-01-24 03:42:13 +01:00
Fabian Dill
e7f8f40464 Factorio: fix automation-level tech costs before automation (#1402)
* Factorio: fix automation-level tech costs before automation

* Factorio: remove double-rolling of science cost
2023-01-24 03:36:50 +01:00
Fabian Dill
847582ff5f Server: fix release_mode (#1407)
* Server: fix release_mode

* Core: actually rename forfeit to release across the program
2023-01-24 03:36:27 +01:00
recklesscoder
1a44f5cf1c CommonClient: Fix address pre-selection (#1406) 2023-01-23 04:59:51 +01:00
Fabian Dill
032bc75070 Core: demote logfile deletion loglevel to debug 2023-01-23 02:23:16 +01:00
Alchav
fb47483212 Pokémon R/B: Fix trainersanity location name 2023-01-23 00:38:38 +01:00
Alchav
d185df3972 Pokémon R/B: Use local random object when randomizing trainer parties in generate_output 2023-01-23 00:38:38 +01:00
lordlou
941dcb60e5 SM: fixed flawed and limited comeback check (#1398)
The issue at hand is fixing impossible seeds generated by a lack of properly checking if the player can come back to its previous region after reaching for a new location, as reported here: https://discord.com/channels/731205301247803413/1050529825212874763/1050529825212874763

The previous attempt at checking for comeback was only done against "Landing Site" and the custom start region which is a partial solution at best. For exemple, generating a single player plando seed with a custom starting location at "red_brinstar_elevator" with a forced red door at "RedTowerElevatorBottomLeft" and 2 Missiles set at "Morphing Ball" and "Energy Tank, Brinstar Ceiling" would generate an impossible seed where the player is expected to go through the green door "LandingSiteRight" with no Supers to go to the only possible next location "Power Bomb (red Brinstar spike room)". This is because the comeback check would pass because it would consider coming back to "Landing Site" enough.

The proposed solution is keeping a record of the last accessed region when collecting items. It would then be used as the source of the comeback check with the destination being the new location. This check had to be moved from can_fill() to can_reach() because the maximum_exploration_state of the AP filler only use can_reach().

Its still not perfect because collect() can be called in batch for many items at a time so the last accessed region will be set as the last collected item and will be used for the next comeback checks.

This was tested a bit with the given exemple above (its now failing generation) and by generating some 8 SM players seed with many door color rando, area rando and boss rando enabled.
2023-01-23 00:36:18 +01:00
Fabian Dill
25756831b7 Core: mark version as 0.3.8 2023-01-21 17:30:30 +01:00
Fabian Dill
9add1495d5 SSL support (#1340) 2023-01-21 17:29:27 +01:00
SonicRPika
34dba007dc Pokemon Red and Blue: Updates to trap weights and tracker support (#1395)
* Added cerulean_cave_condition to fill_slot_data

Added `cerulean_cave_condition` to the `fill_slot_data` function, for a poptracker feature being worked on as it was missing

* Added the potential for any traps to be disabled

Adding the ability to disable any kind of trap, for example if you want any status trap except being Poisoned. Will add a contingency to not try and roll a trap if they are all set to disabled.

* Added contingency to if all traps are disabled

Added a contingency to creating items such that it doesn't try to create a trap if all the traps are disabled

* Updated variable name

Edited name of variable to follow PEP 8 variable naming conventions
2023-01-20 18:49:12 +01:00
Fabian Dill
02d3eef565 Core: convert mixture of Plando Options and Settings into just Options 2023-01-19 17:20:23 +01:00
Fabian Dill
c839a76fe7 LttP: allow hinting and tracking "Take Any" type shops (#1392)
* LttP: allow hinting and tracking "Take Any" type shops
fix broken behaviour since bow/cave split


Co-authored-by: CaitSith2 <d_good@caitsith2.com>
2023-01-19 16:17:43 +01:00
alwaysintreble
29e1c3dcf4 LTTP: fix open pyramid for real this time (#1393) 2023-01-19 16:17:16 +01:00
recklesscoder
f6616da5a9 Docs/Subnautica: Updated console instructions, misc clarifications (#1394) 2023-01-19 16:09:08 +01:00
Fabian Dill
8678e02d54 Subnautica: correct doc string placement for early seaglide 2023-01-19 00:03:26 +01:00
Fabian Dill
2f37bedc92 Tests: ensure item name groups do not collide with item names (#1074) 2023-01-18 15:45:48 +01:00
Alchav
91fdfe3e17 Pokémon R/B: Add inheritance to "Completely Random" option as well 2023-01-18 04:26:40 +01:00
Alchav
a41b0051a6 Pokémon R/B: Fix TM/HM compatibility bug 2023-01-18 04:26:40 +01:00
alwaysintreble
b8abe9f980 Tests: add a test to check for dupe locations (#1378) 2023-01-15 20:18:32 +01:00
alwaysintreble
dd3ae5ecbd core: write the plando settings to the spoiler log (#1248)
Co-authored-by: Zach Parks <zach@alliware.com>
2023-01-15 18:10:26 +01:00
PoryGone
e96602d31b SA2B: Fix Gate region connections (#1384) 2023-01-15 17:55:36 +01:00
el-u
81d953daa3 alttp: add item rules for prize locations (#1380) 2023-01-14 14:29:54 +01:00
Alchav
bd774a454e Pokémon R/B: Fix Safari Zone Gate bug (#1381) 2023-01-14 04:59:09 +01:00
espeon65536
ca724c92ad oot: force itempool to higher settings if required by heart logic 2023-01-13 23:53:13 +01:00
espeon65536
11eebbbd32 Ocarina of Time: 0.3.7 hotfixes round 2 (#1351)
* oot: repair closed forest + dungeon ER

* oot: finally skip triforce pieces in balancing

* oot: fix mq_dungeons_mode set to mq or count

* oot: force 0.3.7 client
hopefully this makes people update

* oot: temp fix for skip-child-zelda crash
eventually I want to decide on a better fix for this though

* oot: remove skip-child-zelda item inside if tree

* oot: fix classification of some thieves hideout locations in tracker

* oot: fix regional shuffle for hideout keys and ganon boss key

* oot: properly attach hints to dungeon locations

* Fix entrance shuffle flag not being set correctly due to new dungeon shuffle option format
2023-01-12 20:20:49 +01:00
Doug Hoskisson
608794cded ZillionClient: fix manual disconnect (#1266) 2023-01-07 10:27:43 +01:00
eudaimonistic
816de5ff02 Docs: code_of_conduct.md (#1350)
Update to point of contact.
2023-01-07 10:24:41 +01:00
Fabian Dill
0b941e2268 LttP: attempt at preventing ghost location checks (#1355) 2023-01-07 10:22:15 +01:00
beauxq
57713cda50 Zillion: minor terrain logic update
standing on a moving walkway requires 2 columns of standing space
2023-01-07 10:12:45 +01:00
Jarno
f56cdd6ec3 Sudoku: Hints will no-longer duplicate (#1371) 2023-01-07 10:10:20 +01:00
t3hf1gm3nt
773c517757 update LTTP player template to add all universal AP options (#1372) 2023-01-07 10:09:33 +01:00
JoshuaEagles
2509b7fa3f SA2B: Add Linux section to setup guide (#1374) 2023-01-07 10:00:19 +01:00
toasterparty
10652d23e0 [OC2] Logic: fixes fails when horde levels/items are excluded from location pool (#1369) 2023-01-05 15:49:50 +01:00
JoshuaEagles
f0bc3d33ac Subnautica: add Linux note to setup guide (#1365) 2023-01-04 15:26:32 +01:00
toasterparty
92d1ed60c6 [OC2] Fix "Moon 1-5" never appearing in level pool (#1366) 2023-01-04 15:21:52 +01:00
Zach Parks
fe2b431821 MultiServer: Remove forced_auto_forfeit (#1363) 2023-01-02 19:26:34 -06:00
Zach Parks
0cc83698f9 Docs: Add special name keywords to docs. (#1353) 2023-01-02 14:42:47 -06:00
Alchav
428f643b07 Pokémon R/B: Fix Pokémon Tower 7F crash (#1362) 2023-01-02 13:29:44 -06:00
Fabian Dill
d4e2b75520 Clients: retry connection with ssl (#1341) 2023-01-02 20:24:54 +01:00
Fabian Dill
96cc7f79dc Subnautica: fix early seaglide 2023-01-02 20:24:14 +01:00
Fabian Dill
bdfbc7e14a Network: allow sending frozenset 2023-01-02 20:23:31 +01:00
Fabian Dill
94c6562f82 Tests: make sure DB overwrite actually takes 2023-01-02 20:23:00 +01:00
alwaysintreble
22fe31a141 Generate: fix default utils options (#1361) 2023-01-02 12:48:31 -06:00
alwaysintreble
72fa19ee1f MultiServer/WebHost: rename all references to forfeit and deprecate it (#1243)
* Webhost: rename all references to forfeit and deprecate it

* needed some renames in multiserver for all the commands to function

* remove forfeit commands

* support forfeit_mode for clients

* rename `forfeit_player` to `release_player`
2023-01-02 12:29:21 -06:00
Zach Parks
d899e918b4 Rogue Legacy: Fix early vendors and architect... again. (#1359) 2023-01-02 12:25:47 -06:00
Zach Parks
33d31c4f0f WebHost: Capitalize Special Range choices to keep consistency. (#1360) 2023-01-02 12:25:33 -06:00
Jarno
9c3c69702a WebHost: Fixed game order by title in Site Map (#1349) 2023-01-02 12:24:08 -06:00
Alchav
dae1a3e0f9 Pokemon R/B: Add Revive to better_shops (#1352) 2023-01-02 12:21:08 -06:00
Alchav
1f1ef10cfe [Pokémon R/B] Fix DeathLink softlock and increment data version (#1348) 2022-12-24 08:25:34 +01:00
Alchav
760af59308 [Pokemon R/B] Fix missing lift key logic 2022-12-23 09:39:21 +01:00
Alchav
3dd7e3e706 [Pokemon R/B] actually implement lose_money_on_blackout 2022-12-23 09:39:21 +01:00
espeon65536
189b129dca oot: repair closed forest + dungeon ER 2022-12-22 06:40:51 +01:00
Fabian Dill
092e8d14ad Core: Make apworlds function mostly before Python 3.10 2022-12-20 17:24:04 +01:00
el-u
4cfc73b582 lufia2ac: rename starting_capsule/starting_party options to default_capsule/default_party 2022-12-19 12:45:49 +01:00
el-u
ff9c11d772 lufia2ac: prevent using party member items if party is full 2022-12-19 12:45:49 +01:00
Alchav
b83aec5c12 [Pokemon R/B] add logic to Fighting Dojo and check for non-vanilla old_man setting for cinnabar gym 2022-12-19 07:04:26 +01:00
Alchav
caf63dd737 [Pokemon R/B] Allow 0 exp setting and add logic rule to Cinnabar Gym to ensure higher level Pokémon are catchable 2022-12-19 07:04:26 +01:00
espeon65536
395d35571c Ocarina of Time 0.3.7 hotfixes (#1336)
- Since multiprocessing and exceptions apparently don't interact well, any exceptions in `generate_output` are handled more gracefully now. The event is always set as well, so the process will no longer hang on an exception there.
- The triforce object file was renamed in 7.0. I didn't change the filename in the code, so it would crash on non-Windows systems.
- Some hint distributions just crash, so I'm temporarily removing them. It will take a while to port the relevant functionality and I'd rather not hold up the next version release.
2022-12-19 07:03:38 +01:00
Fabian Dill
e0be79639c Subnautica: 2.0 compatibility (#1329) 2022-12-17 17:42:02 +01:00
Fabian Dill
37b7f0d32d WebHost: fix LttP tracker crash 2022-12-17 15:22:29 +01:00
el-u
50677ee6a2 lufia2ac: document changes from the vanilla game 2022-12-17 09:26:26 +01:00
PoryGone
f8bc3359c7 SMW: Adjust Butter Bridge 2 - Dragon Coins logic (#1332) 2022-12-17 09:18:23 +01:00
toasterparty
6e537e17e6 [OC2] New Option - Shorter Horde Levels (#1328) 2022-12-16 16:52:15 +01:00
toasterparty
e853fc208b [OC2] Remove DLC items from item pool if vanilla level order (#1323) 2022-12-16 16:48:57 +01:00
toasterparty
1a36da33b4 [OC2] Horde Levels Logically Give 1-Star (#1322) 2022-12-16 16:48:12 +01:00
0rganics
56fc614588 SC2: Bugfix for web tracker (#1324) 2022-12-13 07:35:12 +01:00
espeon65536
47f1fcf382 OoT: Fix region_has_shortcuts crash when boss shuffle is on (#1325) 2022-12-13 07:34:17 +01:00
el-u
51c6be047f Lufia II Ancient Cave: implement new game (#1218)
Co-authored-by: wordfcuk <greili1985@gmail.com>
2022-12-12 02:36:18 +01:00
Fabian Dill
2c46c48ba9 WebHost: reduce tracker refresh delay (#1290) 2022-12-12 00:30:43 +01:00
Alchav
32820ba653 Pokemon R/B: Bug fixes and add trap weights (#1319)
* [Pokemon R/B] Move type chart rando to generate_early and add trap weights

* [Pokemon R/B] Update patching process on client to verify hash
2022-12-11 13:51:28 -06:00
Fabian Dill
6173bc6e03 Core: move create_playthrough under Spoiler as method (#1310)
Core: split create_playthrough, allowing skipping of paths
2022-12-11 13:48:26 -06:00
Magnemania
e71ea94fe5 SC2: Various bugfixes (#1267)
* SC2: Fixed nondeterminism resulting from early unit placement

* SC2: Renamed "world" arguments to "multiworld"

* SC2: Fixed All-In Ground including anti-air in defense score, fixed error in beats_protoss_deathball

* SC2: Fixed No Logic using logic on Beat events

* SC2: Fixed /unfinished command failing when All-In available
2022-12-11 13:46:24 -06:00
0rganics
e3f169b4c3 WebHost: Add SC2WoL game specific tracker (#1270) 2022-12-11 13:43:31 -06:00
kindasneaki
e4e74074f0 Docs: change connecting to archipelago in RoR2 setup guide (#1320)
* Change archipelago mod download page

* Docs: change connecting to archipelago in RoR2 setup guide
2022-12-11 13:38:11 -06:00
Fabian Dill
149630d532 Docs: add remote_start_inventory info in generate_output (#1316) 2022-12-11 14:14:27 +01:00
Fabian Dill
2dcfbff751 Tests: now autoload tests from /worlds/*/test (#1318)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-12-11 13:15:23 +01:00
PoryGone
ec45479c52 SMW: Fix some stages walking Mario onto invalid tile on recomplete (#1317)
If you beat a stage that was already beaten, Mario would sometimes walk onto a tile that he shouldn't.
2022-12-11 04:14:04 +01:00
espeon65536
aee0df5359 Ocarina of Time 7.0 (#1277)
## What is this fixing or adding?
- Adds the majority of OoTR 7.0 features:
  - Pot shuffle, Freestanding item shuffle, Crate shuffle, Beehive shuffle
  - Key rings mode
  - Dungeon shortcuts to speed up dungeons
  - "Regional" shuffle for dungeon items
  - New options for shop pricing in shopsanity
  - Expanded Ganon's Boss Key shuffle options
  - Pre-planted beans
  - Improved Chest Appearance Matches Contents mode
  - Blue Fire Arrows
  - Bonk self-damage
  - Finer control over MQ dungeons and spawn position randomization
- Several bugfixes as a result of the update:
  - Items recognized by the server and valid starting items are now in a 1-to-1 correspondence. In particular, starting with keys is now supported.
  - Entrance randomization success rate improved. Hopefully it is now at 100%. 

Co-authored-by: Zach Parks <zach@alliware.com>
2022-12-11 04:11:40 +01:00
Fabian Dill
2cdd03f786 Network: implement 0.4 marked compatibility removals (#757)
* world remote items handling
* players list when connecting
2022-12-11 02:59:17 +01:00
Zach Parks
ce42fda85f Rogue Legacy: Fix early vendors and architect not being added to pool. (#1314) 2022-12-10 17:24:05 -06:00
Fabian Dill
78a18dee4e Subnautica: give Early Seaglide a display name (#1313) 2022-12-10 11:00:18 -06:00
beauxq
b7d46004e2 Zillion: fix invalid slot data from race condition 2022-12-10 11:15:19 +01:00
Jarno
c3fe341736 Docs: slot_data typing (#1300)
* Docs: slot_data typing

* Properly escaped brackets [ ]
2022-12-09 10:24:08 +01:00
Fabian Dill
79bb43b77c Core: embed custom datapackage into .archipelago (#1288)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-12-08 21:23:31 +01:00
Zach Parks
bedc78d335 Rogue Legacy: Remove relative imports and move to .apworld. (#1304)
* Remove relative import and remove `set_rule` usage.

* Set Rogue Legacy to be .apworld.
2022-12-08 08:54:49 -06:00
t3hf1gm3nt
1b582e5b09 docs: update pokemon and oot setup guides about changing lua options (#1303)
- Pokemon guide was missing a point about changing the Lua option in Bizhawk.
- The same point in the OoT guide was missing a step about restarting Bizhawk after changing the Lua option, so updated that as well.
2022-12-08 10:52:52 +01:00
Alchav
f278dd95c5 [Pokémon R/B] Major bug fix + dialogue change (#1305)
* [Pokemon R/B] Major bug fixes

* [Pokemon R/B] Dialogue updates
2022-12-08 10:51:01 +01:00
PoryGone
92f75f3e03 SA2B: Add missing Whistle location (#1306) 2022-12-08 10:47:39 +01:00
Jarno
f5adc7bdc5 docs: world api fixed link (#1299) 2022-12-08 02:57:49 +01:00
Fabian Dill
78d4da53a7 Tests: verify and fix host.yaml/Utils.py match (#1302) 2022-12-08 02:06:34 +01:00
Alchav
e206c065bf Pokémon Red and Blue: Version 2 (#1282)
Adds Trainersanity option (Each non-scripted trainer has a location check, adding 317 locations)
Adds Randomize Pokedex option. It is required to obtain items from Oak's Aides.
Adds option to add all normal shop items to all normal shops.
Adds DeathLink option.
Adds traps.
Improves Type Chart randomization.
Items can be received during battle.
Stores start inventory in ROM. Only requests remote start inventory if patch is from v1.
Fixes logic bugs.
Various other improvements.
2022-12-08 00:38:34 +01:00
Fabian Dill
5273812039 Core: default distribute Factorio and Subnautica as .apworld (#1260) 2022-12-06 23:40:30 -06:00
Fabian Dill
7c3af68e59 ItemLinks: allow linking replacement items as well (#1274) 2022-12-06 23:37:47 -06:00
PoryGone
449973687b SA2B: v2.0 Content Update (#1294)
Changelog:

Features:
- Completely reworked mission progression system
  - Control of which mission types can be active per-gameplay-style
  - Control of how many missions are active per-gameplay-style
  - Mission order shuffle
- Two new Chaos Emerald Hunt goals
  - `Chaos Emerald Hunt` involves finding the seven Chaos Emeralds and beating Green Hill
  - `FinalHazard Chaos Emerald Hunt` is the same, but with the FinalHazard fight at the end of Green Hill
- New optional Location Checks
  - Keysanity (Chao Containers)
  - Whistlesanity (Animal Pipes and hidden whistle spots)
  - Beetlesanity (Destroying Gold Beetles)
- Option to require clearing all active Cannon's Core Missions for access to the Biolizard fight in `Biolizard` goal
- Hard Logic option
- More Music Options
  - Option to use SADX music
  - New `Singularity` music shuffle option
- Option to choose the Narrator theme 
- New Traps
  - Tiny Trap is now permanent within a level
  - Gravity Trap
  - Exposition Trap
  
Quality of Life:
- Significant revamp to Stage Select screen information conveyance
  - Icons are displayed for:
    - Relevant character's upgrades
    - Which location checks are active/checked
    - Chaos Emeralds found (if relevant)
    - Gate and Cannon's Core emblem costs
  - The above stage-specific info can also be viewed when paused in-level
    - The current mission is also displayed when paused
- Emblem Symbol on Mission Select subscreen now only displays if a high enough rank has been gotten on that mission to send the location check
- Hints including SA2B locations will now specify which Gate that level is located in
- Save file now stores slot name to help prevent false location checks in the case of one player having multiple SA2B slots in the same seed
- Chao Intermediate and Expert race sets are now swapped, per player feedback
  - Intermediate now includes Beginner + Challenge + Hero + Dark
  - Expert now includes Beginner + Challenge + Hero + Dark + Jewel
- New mod config option for the color of the Message Queue text

Bug Fixes:
- Fixed bug where game stops properly tracking items after 127 have been received.
- Several logic fixes
- Game now refers to `Knuckles - Shovel Claws` correctly
- Minor AP World code cleanup
2022-12-07 06:20:02 +01:00
Fabian Dill
f5638552cc Docs: add ClassVar marker to World class (#1224) 2022-12-06 23:12:56 -06:00
Fabian Dill
78ee19de51 Pokemon R/B: always bind file handling to client (#1261) 2022-12-06 23:02:54 -06:00
Zach Parks
82444229be Rogue Legacy: More refactoring and clean up. (#1297)
* Rogue Legacy: More refactoring and clean up.

* Also marked Blacksmith as Progression as it's used in a rule.

* Remove extra newline.

* Prevent divide by zero type error.

* Scratch last commit, got the math mixed in my head.

* Clarified name of rule regarding percentage of stat upgrades.

* Move early vendors/architect creation into `create_items` logic.

* Rename parameter in `create_region`.

* Rename local var in `create_region`.

* Removed accidental links in Markdown docs.

* Refactor `create_region` signature and caller.

* Remove redundant if-else.

* Revert change to if-else, and moved item_pool to function instead of obj var.

* Rename LegacyLogic to RLLogic.

* Remove LogicMixin for rules.
2022-12-06 20:49:55 -06:00
alwaysintreble
2cc03d003a Core: fix bug that caused world option overrides to fail (#1293)
* core: fix bug that caused world option overrides to fail

* copy paste sliver's better code that works as intended

* Fix whitespace

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-12-06 00:50:11 +01:00
recklesscoder
0e4fa378dd WebHost: Detect confusion of settings zip and seed zip (#1227) 2022-12-06 00:40:51 +01:00
Fabian Dill
ffc000ec91 Network: remove deprecated IgnoreGame tag 2022-12-05 23:20:19 +01:00
Fabian Dill
32b8f9f9f3 WebHost: restore old silent ignore of mimetypes in json getting (#1292)
* WebHost: restore old silent ignore of mimetypes in json getting of /api/generate

* Tests: add tests for /api/generate
2022-12-05 22:27:15 +01:00
Yoshi348
4412434976 meta.yaml: update progression balancing (#1283)
* Update meta.yaml to numeric progression balancing

Instead of the old on/off system

* normal/disabled
2022-12-05 22:26:44 +01:00
espeon65536
9bdbced51f Hylics 2: create victory location earlier to ensure it is cached correctly (#1291)
Fixes generation issues where the victory location could not be found from MultiWorld.get_locations
2022-12-04 21:04:01 -06:00
Fabian Dill
bd574ef261 WebHost: save datatables state (#1145)
* WebHost: save datatables state

* WebHost: Fix DataTables local storage keys.

Co-authored-by: recklesscoder <57289227+recklesscoder@users.noreply.github.com>
2022-12-04 20:39:07 -06:00
BordynConfused
45719eb7e0 Hylics2:Logic Fixes (#1281)
Added Juice Ranch and Worm Pod to logic

Extra parenthesis removed

* Delete log.txt
Not used

Transitioned all whitespace characters to Linux /n from Windows /r/n

Updated rules for Rescue Blerol 1 and 2 to avoid impossible seed generation.  Fixed member requirements for Vault Bomb. Added Juice Ranch: Fridge for consistency with other checks behind combat. Added party_shuffle rules to vault medallions, removed jail key requirement (not needed)
2022-12-05 02:25:16 +01:00
kindasneaki
d81fd280fa RoR2: Change risk of rain mod download page link (#1269) 2022-12-04 19:16:03 -06:00
Fabian Dill
6b57275859 Server: attempt to make nothing found message clearer (#1289) 2022-12-04 19:06:13 -06:00
Jarno
63f012cce7 Timespinner: Enter Sandman flag (#1263) 2022-12-04 22:02:46 +01:00
Fabian Dill
679cb3e197 Factorio: fix revealed tech tree hinting old location names 2022-12-04 21:29:35 +01:00
Fabian Dill
38b5a90c07 WebHost: update modules 2022-12-04 21:29:02 +01:00
Fabian Dill
203f17f0f6 Subnautica: prompt users to update 2022-12-04 21:28:44 +01:00
Fabian Dill
65995cd586 Network: implement read_only datastore keys: hints and slot_data (#1286)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-12-03 23:29:33 +01:00
PoryGone
64e2d55e92 SMW: Bug+logic fixes (#1279)
* Allow levels to scroll vertically without climb or run

* Account for needing Yoshi for VB2 Dragon Coins

* Don't allow messages on intro level
2022-12-02 06:25:02 +01:00
lordlou
ef66f64030 SMZ3: shop check fix (#1257) 2022-12-01 03:51:29 +01:00
NewSoupVi
e641c3ca1b The Witness: Fix Expert PP2 Access Logic 2022-12-01 03:46:11 +01:00
alwaysintreble
111c3186bd Core: change signatures in autoworld from world to multiworld (#1273) 2022-12-01 03:45:22 +01:00
toasterparty
f0e9080108 [OC2] Difficulty Adjustment/Documentation (#1278)
* Slightly relax difficulty of final level

* Table to help translate player skill to yaml setting
2022-12-01 03:44:08 +01:00
Jarno Westhof
fd8867c782 [Witness, Docs] Updated link to repository owner, as repository was transferred 2022-12-01 03:42:49 +01:00
Fabian Dill
f81d2653e0 Subnautica: implement swim_rule option and skip goal prog balancing (#1258) 2022-11-28 07:43:04 +01:00
Fabian Dill
1288f15e45 Core: Fill fix local logic conflict (#1271) 2022-11-28 07:03:09 +01:00
Fabian Dill
cde2a6e754 Core: log additional system info that may be relevant to errors (#1255) 2022-11-27 19:52:36 -06:00
Jarno
81dd1e359b Sudoku: Updated text about duplicated hints now the client has been improved (#1264) 2022-11-27 19:30:35 -06:00
alwaysintreble
8dffd87bee LttP: fix open pyramid settings parsing (#1253)
* lttp: fix open pyramid settings prasing

* accidentally left default changed when committing
2022-11-27 19:29:18 -06:00
Fabian Dill
67be80e59d Docs: datapackage typing (#1229)
* Docs: add ClassVar marker to World class

* Docs: add typing to network_data_package
2022-11-27 19:25:53 -06:00
recklesscoder
ff1f5569e7 Factorio: Option descriptions, peaceful mode setup, docs (#1233)
* Docs/Factorio: Flesh out lacking option descriptions.

* Docs/Factorio: Added instructions on peaceful mode.

* Docs/Factorio: Use subheadings for each "other setting".

* Docs/Factorio: Mention that `tech_tree_information: full` grants hints.

* Docs/Factorio: Instructions to use .yaml checker after editing your .yaml manually.
2022-11-27 04:01:10 +01:00
NewSoupVi
8b9b482972 The Witness: Logic Fix (Broken Seed reported)
Jungle Discard requires Arrows, not Triangles
2022-11-27 02:08:56 +01:00
tctompk
d0ce44cd38 Pokemon RB: Fix broken link in Pokemon setup instructions (#1254)
The setup_en.md file had an un-escaped URL that was breaking a link; this fixes the link by escaping the spaces in the URL.
2022-11-24 21:11:46 +01:00
PoryGone
aae78a8a12 Core: Add MultiServer command to check a specific location (#1242) 2022-11-20 22:38:34 +01:00
Fabian Dill
7a5e11e8d4 Core: streamline Multiworld.get_*_locations calls (#1234) 2022-11-20 20:50:32 +01:00
Zach Parks
a9ab53cb8b WebHost/Core: Defer creating slots that have patch files until after multidata is loaded and remove redundant code. (#1250)
* WebHost: On uploads, infer player name if missing in file name.

* Remove conditional logic for not including player name in file name.

* quick readability tweak to "fix"

* Refactored `upload_zip_to_db` to clean up redundancies and fix issues

* Rename `patches` to `files`

* fix comment
2022-11-20 13:39:52 -06:00
Fabian Dill
5ed8c2e1c0 Core: datetime-tag log files and delete old ones 2022-11-20 18:43:44 +01:00
Fabian Dill
67128ece38 LttP: make xxtea only required for race generation 2022-11-20 18:43:26 +01:00
Rome Reginelli
8aed24151f SNIClient: fix SNI not launching with old host.yaml (#1249)
This happens when switching from Appimage 0.3.5 to Appimage 0.3.6 without deleting the user's host.yaml
2022-11-20 11:14:13 +01:00
black-sliver
3e6c097348 SoE: update source wheel for py3.11 on windows 2022-11-18 01:43:53 +01:00
Fabian Dill
8ce3fd5518 Core: update cx-Freeze 2022-11-17 23:53:50 +01:00
Alchav
93a354cd81 [Core] Item plando early locations (and non-early locations) (#1228) 2022-11-17 17:40:44 +01:00
espeon65536
774581b7ba HK: fix crash if shop locations are at max and extra shop slots is nonzero 2022-11-17 17:36:42 +01:00
NewSoupVi
95f90851ac The Witness: Update docs, credits, junk hints (#1240)
* Updated Credits & AP-game hints

* Updated docs
2022-11-17 17:35:59 +01:00
PoryGone
1cd1bfea4d SMW: Prevent Killing Bowser on Yoshi Egg Hunt (#1241)
* Make Bowser unkillable on Egg Hunt

Changed a location name.
2022-11-16 20:20:55 +01:00
espeon65536
edd1fff4b7 Core: make early_items internal only (#1177)
Co-authored-by: beauxq <beauxq@yahoo.com>
2022-11-16 17:32:33 +01:00
Zach Parks
4d79920fa6 Merge pull request #944
* WebHost: Remove "Random" as an option and move to separate button in …

* Merge branch 'main' into randomize-button

* Tweaked color and changed text of tooltip.

* Merge branch 'main' into randomize-button
2022-11-12 22:03:44 -05:00
recklesscoder
7665935227 Merge pull request #1231
* Web/Style: Fix code block padding.
2022-11-12 21:58:11 -05:00
Zach Parks
5139475068 Slay the Spire: Correct tool tip for heart_run to match actual behaviour (#1236) 2022-11-12 18:52:36 -08:00
PoryGone
adcee639a2 SMW: Adjust Early Double Levels (#1238) 2022-11-13 01:33:22 +01:00
Magnemania
fde97fca5b SC2: Increment required client version (#1237) 2022-11-13 01:32:33 +01:00
recklesscoder
e108b67ca5 Docs/OoT: Add savestate fix and links to Bizhawk and Logic wiki pages. 2022-11-12 02:19:26 +01:00
NewSoupVi
17da06f763 The Witness: Fix "doors: panels" items referring to the wrong panels 2022-11-12 02:18:51 +01:00
PoryGone
2ff737175f SMW: Fix Baby Yoshi and Rope Jump Exploits (#1226)
* Fix Baby Yoshi and Rope Jump Exploits

* Fix logic error of Forest of Illusion 3 Secret Exit
2022-11-11 11:50:29 +01:00
NewSoupVi
b0b8268249 Witness: Expert logic broken seed fixes (by IHNN) (#1223)
Co-authored-by: metzner <unconfigured@null.spigotmc.org>
2022-11-10 04:05:00 +01:00
espeon65536
4e5c10ad66 OoT: make Bottles and Adult Trade Item hintable groups (#1222)
* OoT: make Bottles and Adult Trade Item hintable groups
2022-11-09 22:07:14 +01:00
Alchav
350e1e6287 Pokemon Red and Blue: lua updates (#1216) 2022-11-09 15:15:16 +01:00
Magnemania
63c0d027e7 SC2: Bugfixes for YAML Option Interactions (#1215)
Early Unit: Now respects Excluded Items when selecting a random unit.

Units Always Have Upgrades: Changed removal cascade behavior; now additionally checks to see if an associated item is already locked when attempting to remove, and locks associated items if so. Occasionally caused issues with starter items in the past, frequently caused issues with yaml-defined Locked Items.
2022-11-09 13:53:55 +01:00
jtoyoda
a014bb4ab7 FF1: Updating FFR Docs for new Archipelago location (#1221) 2022-11-08 18:58:29 -06:00
black-sliver
0d10fec395 DS3: update data_version (#1220) 2022-11-09 01:53:08 +01:00
Ludovic Marechal
0cbee4ac3e DS3: Add progressive locations, fix the randomize_weapons_level option and add some options ( Deathlink ) (#1206)
* Update items_data.py

added `Red and White Round Shield`, `Crystal Scroll`, `Magic Stoneplate Ring`, and `Outrider Knight` gear.

* Update locations_data.py

Added `US: Red and White Round Shield`, `CKG: Magic Stoneplate Ring`, `GA: Outrider Knight` set, and `GA: Crystal Scroll`

* Update __init__.py

Add `Karla's Ashes` requirements

* Update items_data.py

Add `Irithyll Rapier, Hollow's Ashes, Irina's Ashes, Karla's Ashes, Cornyx's Ashes, and Orbeck's Ashes`

* Update locations_data.py

Add `Irithyll Rapier, Hollow's Ashes, Irina's Ashes, Karla's Ashes, Orbeck's Ashes, and Cornyx's Ashes`

* Update items_data.py

removed "hollows ashes"

* Update locations_data.py

remove "hollows ashes"

* Revert "WebHost: Add the DarkSouls3 entry to upload and download the client file"

This reverts commit 5e7c2d4cee.

* ds3: setup progressive locations

* ds3: Use fill_slot_data instead of generate_output

* ds3: Add no_spell_requirements, no_equip_load and death_link option

* ds3: Add some progressive locations

* DS3: Increment data_version

* DS3: Fix item name in rule

* DS3: Set required client version to 0.3.6 and added offsets between items and location tables for backward compatibility

* DS3: Resolve Python 3.8 compatibility

* DS3: Removed useless region for locations IDs consistency

* DS3: Changed i in loop

* DS3: Remove AP.json from the documentation

* DS3: Put back json upload and download

* DS3: Avoid empty downloads

* DS3: Fix randomize_weapons_level option

* DS3: Remove options duplicate entries

* DS3: Change location rule according to review

Co-authored-by: Br00ty <83629348+Br00ty@users.noreply.github.com>
2022-11-09 01:17:43 +01:00
toasterparty
70cab99caf OC2: Fix typo in description (#1214) 2022-11-07 19:50:30 +01:00
Fabian Dill
c1e97bcbff WebHost: allow setting a generation time limit (#1209)
* WebHost: allow setting a generation time limit.
2022-11-06 21:37:11 +01:00
shadow42085
e2eaafbf70 WebHost: Update Super Metroid Tracker Links (#1211)
varia randomizer site updated
2022-11-06 14:31:02 -06:00
NewSoupVi
66d594e95b The Witness: Add required client version (#1212)
Co-authored-by: metzner <unconfigured@null.spigotmc.org>
2022-11-06 14:26:56 -06:00
Fabian Dill
a9bf0008ba Factorio: add rocket-silo as required technology (#1207) 2022-11-06 14:24:03 -06:00
NewSoupVi
f2426ae603 The Witness: Logic Fix (#1210)
Co-authored-by: metzner <unconfigured@null.spigotmc.org>
2022-11-06 17:31:53 +01:00
Zach Parks
462ddce72c Templates: Remove auto wordwrap, fix manual indentation, and reformat docstrings for world options in main. (#1201)
* Fix wrapping too early if docstring is within 120 characters and re-indent other lines.

* Remove auto-word wrapping and tweaked all worlds' option docstrings formatting.

Options should wrap around the 120 character mark to prevent template files from being too long horizontally. This also allows manual-indentation to work again.

* Fix missing '#' on empty lines in output from docstring.
2022-11-06 08:28:16 -06:00
Alchav
d10bb3c6c1 [Pokémon Red and Blue] more improvements (#1208)
* Generated patch includes base patch

* location ID range start match item ID start

* remove unused import

* Change Oak's Aides defaults to be more sync-friendly
2022-11-06 09:07:41 +01:00
Fabian Dill
61232ca756 Factorio: set useless technologies to be researched from the start (#1205) 2022-11-05 14:01:02 -05:00
AkumaGath17
8f325a4f2b [Minecraft] AP Tracker (Items & Advancements) Update to 1.19 (#1168)
* Tracker hud test

* Added Relevant Items Icon

* Minecraft Tracker Update Test

* Minecraft Tracker Update

* Minecraft Tracker Missing Advancements

* Removed Enchanted Books

* Revert fix

* Added Relevant Books

* Tracker Update

* Minecraft Tracker Update (Saddle 3D Model to Icon)

Co-authored-by: AkumaDark17 <akumaspartanmc@hotmail.com>
2022-11-05 06:41:03 -07:00
NewSoupVi
d28738a918 The Witness: Logic Fix & Generation Fix (#1204)
* Logic fix

* static object modification instead of copy error fixed

Co-authored-by: metzner <unconfigured@null.spigotmc.org>
2022-11-04 23:10:29 -05:00
PoryGone
1f3d048462 DKC3: Fix Generation Hang from missed self.world reference (#1203)
* Baseline patching and logic for DKC3

* Client can send, but not yet receive

* Alpha Test Baseline

* Bug Fixes and Starting Lives Option

* Finish BBH, add world hints

* Add music shuffle

* Boomer Costs Text

* Stubbed in Collect behaviour

* Adjust Gyrocopter option

* Add Bonus Coin junk replacement and tracker support

* Delete bad logs

* Undo host.yaml change

* Refactored SNIClient

* Make Swanky Free

* Fix Typo

* Undo SNIClient run_game hack

* Fix Typo

* Remove Bosses from Level Shuffle

* Remove duplicate kivy Data

* Add DKC3 Docs and increment Data version

* Remove dead code

* Fix mislabeled region

* Add Dark Souls 3 to README

* Always force Cog on Rocket Rush Flag

* Fix Single Ski lock and too many DK Coins

* Update Retroarch version number

* Don't send DKC3 through LttP Adjuster

* Comment Location ROM Table

* Change ROM Hash prefix to D3

* Remove redundant constructor

* Add ROM Change Safeguards

* Properly mark WRAM accesses

* Remove outdated region connect

* Fix syntax error

* Fix Game description

* Fix SNES Bank Access

* Add isso_setup for DKC3

* Double Quote strings

* Escape single quotes I guess

* Add two locations to Trade Sequence List

* Remove trace sequence locations from ROM data dict

* Prevent Krematoa Crash, add crash robustness

* Remove print statements

* Don't remove ctx.rom if save file dies

* Consolidate logic for readability

* Add link to tracker on DKC3 Setup doc

* Remove false information from setup guide

* Fix file extension in setup doc

* Bug Fixes

* Add KONGsanity and cheat options

* First Pass Collect behavior

* Fix level unlock data

* Make ! only indicate KONG letters on KONGsanity

* Fix Level Name in locations

* Adjust junk pool logic

* Fix Knautilus Connections

* Fix Wrinkly Softlock

* Fix missed world reference
2022-11-04 21:40:31 -05:00
PoryGone
b161a5241f SMW: Add link to Tracker in setup guide (#1202) 2022-11-04 21:36:58 -05:00
Fabian Dill
208a0c6b08 WebHost: prevent infinite spinup loop of Rooms 2022-11-04 20:07:28 +01:00
Jarno
c3c1ce5827 Sudoku: Fixed setup link (#1200) 2022-11-04 20:03:39 +01:00
recklesscoder
889bc9d1b4 FactorioClient: Warn about Windows console input. 2022-11-04 18:00:22 +01:00
recklesscoder
165a38dd58 Factorio: Added commands for checking energy link from CLI and in game. 2022-11-04 17:59:05 +01:00
recklesscoder
88088dd054 CommonClient & SNIClient: Fixes for reconnecting (#1193)
* CommonClient & SNIClient: Fixes for reconnecting.
- CommonClient: Allow manual reconnect by typing /connect.
- CommonClient: Don't prompt to reconnect if there is nothing to reconnect to.
- CommonClient: Hide the connection loss modal popup when attempting to connect again.
- CommonClient & SNIClient: Cancel auto-reconnect tasks when the user intervenes.

* (Fix imports for linting.)
2022-11-04 17:57:58 +01:00
Doug Hoskisson
c933fa7e34 Core: optimize early items and add unit test (#1197)
* optimize early items and add unit test

* move sorting list init closer to sorting
2022-11-04 17:56:47 +01:00
alwaysintreble
f1123f2662 Core: more useful error for entrance without region (#1186) 2022-11-03 15:17:34 +01:00
Fabian Dill
0f034ddcf7 Factorio: make client use CommonClient location lookup for technologies 2022-11-03 14:19:47 +01:00
recklesscoder
7f3eda4623 Subnautica: Fix creature_scan_logic: removed allowing Cuddlefish. 2022-11-03 10:48:50 +01:00
alwaysintreble
2b0e7f05da Docs: fix broken link in contributing.md (#1185) 2022-11-02 22:02:06 -05:00
recklesscoder
e204deab02 Factorio: Fix saving on exit on Windows.
Fix detecting of server shutdown.
2022-11-03 00:17:26 +01:00
Magnemania
56afd62175 SC2: Bugfix, incorrectly adding to an array rather than appending (#1183) 2022-11-02 17:01:51 -05:00
Alchav
44204ac9be Pokemon Red/Blue: Fix location name in Pokémon Red and Blue rules (#1184) 2022-11-02 17:01:32 -05:00
espeon65536
6c3852a2a9 OoT: lock fast-filled shop locations (#1188)
ensures that progression balancing doesn't move them later
2022-11-02 17:01:03 -05:00
Jarno
124ae198e4 Sudoku: Updated Sudoku docs now we use standalone deployment (#1180) 2022-11-02 20:21:34 +01:00
alwaysintreble
030b767751 LttP: fix triforce piece same reference (#1179) 2022-11-02 20:08:38 +01:00
NewSoupVi
5ca724a454 The Witness: Logic fix
A broken seed was reported where Colored Squares was on a panel locked by Colored Squares. This PR fixes this problem.
2022-11-02 20:07:43 +01:00
Jarno
af3b752093 WebHost: Fixed warning ".gitignore dropped, as it has no valid sprite data." (#1174)
* Fixed warning ".gitignore dropped, as it has no valid sprite data." on webhost

* Changed to exclude files starting with .
2022-11-02 19:06:00 +01:00
alwaysintreble
c378933274 Overcooked 2: Revert OC2Level world references to be named correctly, optimize location scouting. (#1175)
* fix oc2 level world references

* optimize the local items discovery a bit
2022-11-02 16:11:46 +01:00
Doug Hoskisson
da392239a0 MultiServer and clients: async coroutine starter in Utils.py (#1143)
* async coroutine starter in Utils.py

* refactor from static class to function

* async_start docstring
2022-11-02 15:51:35 +01:00
espeon65536
a6e1e14fee Ocarina of Time: Itemlinks and bugfixes (#1157)
* OoT: ER improvements
Include dungeon rewards in itempool to allow for ER improvement
Better validate_world function by checking for multi-entrance incompatibility more efficiently
Fix some generation failures by ensuring all entrances placed with logic
Introduce bias to some interior entrance placement to improve generation rate

* OoT: fix overworld ER spoiler information

* OoT: rewrite dungeon item placement algorithm
in particular, no longer assumes that exactly the number of vanilla keys is present, which lets it place more or fewer items.

* OoT: auto-send more locations
Now should autosend cows, DMT/DMC great fairies, medigoron, and bombchu salesman
This should be every check autosending. these ones are super weird for some reason and didn't get fixed with the others

* OoT: add items forced local by settings to AP's local_items

* OoT: fast-fill shop junk items

* OoT: ensure that Kokiri Shop is always reachable immediately in closed forest
hence Deku Shield can be bought to leave the forest

* OoT: randomize internal connect name
Connect name is now a random 16-character string.
This should prevent any issues with connecting to a room with the wrong ROM with probability almost 1.

* OoT: introduce TrackRandomRange for trials hint and mq dungeon maps

* OoT: enable proper itemlinking of songs and dungeon items, with restricted placements according to player settings

* OoT: barren hint oversight fix

* OoT: allow NL + ER to roll properly

* OoT: 3.8 compatibility
set and list builtins don't have proper typing support until 3.9, apparently

* OoT: remove Gerudo Membership Card location from the pool if fortress open and card not randomized
another long-standing bug squished

* OoT: exclude locations in the itemlink song fill if they aren't also priority

* OoT: prevent data bleed when client isn't closed between different game connections
I don't understand why people keep doing this

* OoT: linter appeasement
it was a real error though

* fixing merge conflicts is hard

* oot merge update #2

* OoT: removed accidentally duplicated code
2022-11-02 09:32:08 +01:00
alwaysintreble
95378233fc Templates: Update template output and add min and max comments for named special_range options. (#1164)
* add min and max comments for named special_range options

* comment all special range options and dictate the min and max in comment block

* make it cleaner

* make it cleanerer

* make it cleanererer

* Reformat template for more consistent comments.

* Fixed missing note on some special settings.

* Small tweak to template.

* Update playerSettings.yaml to match auto-generated template with all ALTTP options.

* Fix edge case with `special_range_cutoff` and revert playerSettings.yaml.

Co-authored-by: Zach Parks <zach@alliware.com>
2022-11-01 18:07:56 -05:00
Fabian Dill
85130f2bbd WebHost: update modules (#1176)
Fix bokeh 3.0.0 attribute removal and touch up stats.py while at it
2022-11-01 17:01:53 -05:00
Ludovic Marechal
ab9f3767e2 DS3: Use slot_data instead of the external Json file (#1155)
* Update items_data.py

added `Red and White Round Shield`, `Crystal Scroll`, `Magic Stoneplate Ring`, and `Outrider Knight` gear.

* Update locations_data.py

Added `US: Red and White Round Shield`, `CKG: Magic Stoneplate Ring`, `GA: Outrider Knight` set, and `GA: Crystal Scroll`

* Update __init__.py

Add `Karla's Ashes` requirements

* Update items_data.py

Add `Irithyll Rapier, Hollow's Ashes, Irina's Ashes, Karla's Ashes, Cornyx's Ashes, and Orbeck's Ashes`

* Update locations_data.py

Add `Irithyll Rapier, Hollow's Ashes, Irina's Ashes, Karla's Ashes, Orbeck's Ashes, and Cornyx's Ashes`

* Update items_data.py

removed "hollows ashes"

* Update locations_data.py

remove "hollows ashes"

* Revert "WebHost: Add the DarkSouls3 entry to upload and download the client file"

This reverts commit 5e7c2d4cee.

* ds3: Use fill_slot_data instead of generate_output

* DS3: Increment data_version

* DS3: Fix item name in rule

* DS3: Set required client version to 0.3.6 and added offsets between items and location tables for backward compatibility

* DS3: Resolve Python 3.8 compatibility

* DS3: Removed useless region for locations IDs consistency

* DS3: Changed i in loop

* DS3: Remove AP.json from the documentation

* DS3: Put back json upload and download

* DS3: Avoid empty downloads

(cherry picked from commit c4c485140d)

Co-authored-by: Br00ty <83629348+Br00ty@users.noreply.github.com>
2022-11-01 22:58:08 +01:00
Zach Parks
bf142b32c9 Rogue Legacy: Rename world to multiworld in local variables and function signatures. (#1169) 2022-11-01 16:14:09 -05:00
lordlou
05c06a57af SMZ3: completion condition fix and more info in spoiler (#1133)
* first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions)

* first working single-world randomized SM rom patches

* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions)

* first working single-world randomized SM rom patches

* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* Fixed multiworld support patch not working with VariaRandomizer's

Added stage_fill_hook to set morph first in progitempool
Added back VariaRandomizer's standard patches

* + added missing files from variaRandomizer project

* + added missing variaRandomizer files (custom sprites)

+ started integrating VariaRandomizer options (WIP)

* Some fixes for player and server name display

- fixed player name of 16 characters reading too far in SM client
- fixed 12 bytes SM player name limit (now 16)
- fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO)
- request: temporarly changed default seed names displayed in SM main menu to OWTCH

* Fixed Goal completion not triggering in smClient

* integrated VariaRandomizer's options into AP (WIP)

- startAP is working
- door rando is working
- skillset is working

* - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off")

* skillset are now instanced per player instead of being a singleton class

* RomPatches are now instanced per player instead of being a singleton class

* DoorManager is now instanced per player instead of being a singleton class

* - fixed the last bugs that prevented generation of >1 SM world

* fixed crash when no skillset preset is specified in randoPreset (default to "casual")

* maxDifficulty support and itemsounds removal

- added support for maxDifficulty
- removed itemsounds patch as its always applied from multiworld patch for now

* Fixed bad merge

* Post merge adaptation

* fixed player name length fix that got lost with the merge

* fixed generation with other game type than SM

* added default randoPreset json for SM in playerSettings.yaml

* fixed broken SM client following merge

* beautified json skillset presets

* Fixed ArchipelagoSmClient not building

* Fixed conflict between mutliworld patch and beam_doors_plms patch

- doorsColorsRando now working

* SM generation now outputs APBP

- Fixed paths for patches and presets when frozen

* added missing file and fixed multithreading issue

* temporarily set data_version = 0

* more work

- added support for AP starting items
- fixed client crash with gamemode being None
- patch.py "compatible_version" is now 3

* commited missing asm files

fixed start item reserve breaking game (was using bad write offset when patching)

* Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it).

fixed crash in SMClient when loosing connection to SNI

* fixed No Energy Item missing its ID

fixed Plando

* merge post fixes

* fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color)

* fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses)

* fixed start item x-ray HUD display

* Fixed start items being sent by the server (is all handled in ROM)

Start items are now not removed from itempool anymore
Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though.
Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified

* fixed settings that could be applied to any SM players

* fixed auth to server only using player name (now does as ALTTP to authenticate)

* - fixed End Credits broken text

* added non SM item name display

* added all supported SM options in playerSettings.yaml

* fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region

did some cleaning (mainly reverts on unnecessary core classes

* minor setting fixes and tweaks

- merged Area and lightArea settings
- made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating
- fixed inverted layoutPatch setting

* added option start_inventory_removes_from_pool

fixed option names formatting
fixed lint errors
small code and repo cleanup

* Hopefully fixed ROR2 that could not send any items

* - fixed missing required change to ROR2

* fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum)

* fixed typo with doors_colors_rando

* fixed checksum

* added custom sprites for off-world items (progression or not)

the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu

* - added missing change following upstream merge

- changed patch filename extension from apbp to apm3 so patch can be used with the new client

* added morph placement options: early means local and sphere 1

* fixed failing unit tests

* - fixed broken custom_preset options

* - big cleanup to remove unnecessary or unsupported features

* - more cleanup

* - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips

- small cleanup

* - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch)

* fixed g4_skip patch that can be not applied if hud is enabled

* - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette)

* - updated basepatch to reflect g4_skip removal

- moved more asm files to SMBasepatch project

* - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed)

* fixed wrong path if using built as exe

* - cleaned exposed maxDifficulty options

- removed always enabled Knows

* Merged LttPClient and SMClient into SNIClient

* added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service

* small doc precision

* - added death_link support

- fixed broken Goal Completion
- post merge fix

* - removed now useless presets

* - fixed bad internal mapping with maxDiff

- increases maxDiff if only Bosses is preventing beating the game

* - added support for lowercase custom preset sections (knows, settings and controller)

- fixed controller settings not applying to ROM

* - fixed death loop when dying with Door rando, bomb or speed booster as starting items

- varia's backup save should now be usable (automatically enabled when doing door rando)

* -added docstring for generated yaml

* fixed bad merge

* fixed broken infinity max difficulty

* commented debug prints

* adjusted credits to mark progression speed and difficulty as Non Available

* added support for more than 255 players (will print Archipelago for higher player number)

* fixed missing cleanup

* added support for 65535 different player names in ROM

* fixed generations failing when only bosses are unreachable

* - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish

* fixed failling generations when using 'fun' settings

Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings

* fixed debug logger

* removed unsupported "suits_restriction" option

* fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP)

* - fixed deathlink emptying reserves

- added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves

* - merged death_link and death_link_survive options

* fixed death_link

* added a fallback default starting location instead of failing generation if an invalid one was chosen

* added Nothing and NoEnergy as hint blacklist

added missing NoEnergy as local items and removed it from progression

* added missing completion_condition when TowerCrystals is lower than GanonCrystals

added Rewards and Medallions infos to spoiler

* Update worlds/smz3/__init__.py

Yes, indeed. Thank you!

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: Zach Parks <zach@alliware.com>
2022-11-01 14:52:04 -05:00
beauxq
0f7adaaf7b Zillion: LAGPL license 2022-11-01 20:40:57 +01:00
Doug Hoskisson
962e48c078 Zillion: fix unreproducible seeds (#1166)
* fix zillion unreproducible seeds

* world to multiworld merge
2022-11-01 14:45:17 +01:00
black-sliver
95ea0541e6 Core: improve fulfills_accessibility performance 2022-11-01 14:08:24 +01:00
black-sliver
0ed3baabd4 Core: add generic handling of excluded locations
Currently there can be locations that are marked as excluded,
but don't have rules to enforce it, while fill has special handling
for excluded locations already.

This change removes special rules, and adds a generic rule instead.
2022-11-01 14:08:24 +01:00
black-sliver
2db55ac50b Core: make fulfills_accessibility deterministic and fix some typing 2022-11-01 14:08:24 +01:00
black-sliver
bea8d37a3c SoE: fix false positives in early sphere collection (#1165) 2022-11-01 13:14:38 +01:00
Alchav
813015e007 [Pokemon] Fixes and updates (#1108)
* [Pokemon] Logic fixes

* [Pokemon] Fix seed name length

* [Pokemon] Location name changes

* [Pokemon] Hidden Item Nurse Bed logic fix

* Badges Needed description update

* Ensure player name does not exceed 16 bytes

* Player name check fix

* Remove unique items in start_inventory from item pool

* Vending Machine Drinks will not be created as filler

* Skip trainer text

* Badges needed for viridian gym text

* Add slot data for trackers

* free fly map in slot data and old_man = vanilla allows more free fly maps

* Re-add mistakenly removed slot data item

* Add tracker link to setup doc

* Doc fix

* Fix base patch

* Change pre_fill to generate_basic so items are pre-filled before item links

* Rename some hidden locations

* missing file from commit and revert one errant location name change
2022-11-01 07:02:15 +01:00
recklesscoder
c1d7abd06e CommonClient: Some GUI polish (#1139)
* CommonClient: Focus text field when requesting input.

* CommonClient: Store and prefill last server address.

* CommonClient: Focus and select portion of server address upon start.

* CommonClient: Don't allow editing of address while connected.

* CommonClient: Don't make pressing Enter in the address bar disconnect you.

* CommonClient: Use TextInput.text_validate_unfocus over jank workaround.

* CommonClient: Fixed hang when closing after failed handshake.

* CommonClient: Made scrollbar wider and interactable.
2022-11-01 06:54:40 +01:00
strotlog
655f287d42 SM: Fix unobtainable items in remote items+item links combo (#1151)
* SM: fix using item links together with remote items

* SM: write 0 index for excess player ids

* some style and minor fixes (strotlog/Archipelago#1)

* more typing in SM patching

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2022-11-01 06:42:11 +01:00
Alchav
802119502d Core: Fix early items bug with priority locations (#1167) 2022-10-31 22:26:55 -05:00
alwaysintreble
2af510328e Core: rename world to multiworld (#931)
* rename references to `Multiworld` in core to `multiworld` instead of `world`

* fix smz3

* fix oot

* fix low hanging fruit

* revert mysteriously broken spacing in world api.md

* fix more randomly broken spacing

* hate

* that better be all of it

* begrudgingly move over smw

* ._.

* missed some worlds

* this is getting tedious now

* Missed some self.world definitions

Co-authored-by: espeon65536 <espeon65536@gmail.com>
Co-authored-by: Zach Parks <zach@alliware.com>
2022-10-31 21:41:21 -05:00
black-sliver
87f4a97f1e Core: make player name case-insensitive 2022-10-30 17:12:00 +01:00
black-sliver
1bb99d391d Factorio: deterministic locations 2022-10-30 17:11:34 +01:00
Zach Parks
1cad51b1af Rogue Legacy: World folder clean up and generation improvements. (#1148)
* Minor cleanup and renaming of some files/functions.

* Rename `LegacyWorld` and `LegacyWeb` to RLWorld and RLWeb.

* Undo accidental change to comment.

* Undo accidental change to comment.

* Restructure Items.py format and combine all tables into one.

* Restructure Locations.py format and combine all tables into one.

* Split boss event items into separate boss entries.

* Remove definitions folder.

* Reformatted __init__.py for Rogue Legacy.

* Allow fairy chests to be disabled.

* Add working prefill logic for early vendors.

* Re-introduce Early Architect setting.

* Revamped rules and regions and can now generate games.

* Fix normal vendors breaking everything.

* Fix early vendor logic and add fairy chest logic to require Dragons or Enchantress + runes.

* Fix issue with duplicate items being created.

* Move event placement into __init__.py and fix duplicate Vendors.

* Tweak weights and spacing.

* Update documentation and include bug report link.

* Fix relative link for template file.

* Increase amount of chest locations in `location_table`.

* Correct a refactor rename gone wrong.

* Remove unused reference in imports.

* Tweak mistake in boss name in place_events.

* English is hard.

* Tweak some lines in __init__.py to use `.settings()` method.

* Add unique id tests for Rogue Legacy.

IDs are mixed around, so let's try to avoid accidentally using the same identifier twice.

* Fix typo in doc.

* Simplify `fill_slot_data`.

* Change prefix on `_place_events` to maintain convention.

* Remove items that are **not** progression from rules.
2022-10-29 22:15:06 -05:00
Doug Hoskisson
09d8c4b912 MultiServer: fix case sensitivity in server commands (#1156)
* fix case sensitivity in server commands

* improve ambiguous match breakout

* worried about accidentally swapping team and slot

* Remove now unused import
2022-10-30 00:41:07 +02:00
Br00ty
ed23a426ec DS3: fix shield item/location (#1158)
* fixed item id

fixed item id for "Blessed Red and White Shield"

* fixed location id

fixed location id for "US: Blessed Red and White Shield"
2022-10-29 19:06:56 +02:00
Ludovic Marechal
c711264d1a DS3: Added a few new items and locations (#1059)
* Update items_data.py

added `Red and White Round Shield`, `Crystal Scroll`, `Magic Stoneplate Ring`, and `Outrider Knight` gear.

* Update locations_data.py

Added `US: Red and White Round Shield`, `CKG: Magic Stoneplate Ring`, `GA: Outrider Knight` set, and `GA: Crystal Scroll`

* Update __init__.py

Add `Karla's Ashes` requirements

* Update items_data.py

Add `Irithyll Rapier, Hollow's Ashes, Irina's Ashes, Karla's Ashes, Cornyx's Ashes, and Orbeck's Ashes`

* Update locations_data.py

Add `Irithyll Rapier, Hollow's Ashes, Irina's Ashes, Karla's Ashes, Orbeck's Ashes, and Cornyx's Ashes`

* Update items_data.py

removed "hollows ashes"

* Update locations_data.py

remove "hollows ashes"

* DS3: Increment data_version

* DS3: Fix item name in rule

* DS3: Set required client version to 0.3.6 and added offsets between items and location tables for backward compatibility

* DS3: Resolve Python 3.8 compatibility

* DS3: Removed useless region for locations IDs consistency

* DS3: Changed i in loop

Co-authored-by: Br00ty <83629348+Br00ty@users.noreply.github.com>
2022-10-29 13:35:33 +02:00
black-sliver
3dfbbc5057 Doc: Clarify annotations in style guide (#1149)
* Doc: Clarify annotations in style guide

* Fix typo

* Update docs/style.md

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2022-10-28 23:02:23 +02:00
Doug Hoskisson
f298b8d6e7 Zillion: validate rescue item links (#1140) 2022-10-28 21:56:50 +02:00
Fabian Dill
53974d568b Factorio: revamped location system (#1147) 2022-10-28 21:00:06 +02:00
black-sliver
ec0389eefb CI: use build_exe in automated build 2022-10-28 20:59:06 +02:00
Ryan
0c54c47023 Docs: Add ArchipelagoRS to the Network Protocol docs (#1153) 2022-10-28 19:24:08 +02:00
CaitSith2
80db8a33af Don't leak info about what exists or not if player can't afford the hint (#1146) 2022-10-28 02:45:18 -07:00
AkumaGath17
e6c6b00109 Minecraft: Two by two logical requirement fix (#1152)
* [Minecraft] Two by two logical requirement fix

* Two by two update

* Two by Two logical fix [Description in order]

* Two by Two fix [Bucket only= False]

Along with the others isolated items checks
2022-10-28 10:34:46 +02:00
Alchav
813ea6ef8b [SM64] Separate Entrance Shuffle pools option and MIPS cost option improvement (#1137)
* Add separate pool option for entrance shuffle and swap MIPS costs if MIPS1Cost is greater

* Changes based on N00by's suggestions

* split into secret_entrance_ids and course_entrance_ids
2022-10-28 01:43:02 +02:00
Doug Hoskisson
c09e089f9d Docs: Zillion: RetroArch core and early_items recommendation. (#1150)
* add Genesis Plus GX to Zillion docs

* zillion early items recommendation
2022-10-28 01:35:18 +02:00
recklesscoder
cfff12d8d7 Factorio: Added ability to chat from within the game. (#1068)
* Factorio: Added ability to chat from within the game.
This also allows using commands such as !hint from within the game.

* Factorio: Only prepend player names to chat in multiplayer.

* Factorio: Mirror chat sent from the FactorioClient UI to the Factorio server.

* Factorio: Remove local coordinates from outgoing chat.

* Factorio: Added setting to disable bridging chat out.
Added client command to toggle this setting at run-time.

* Factorio: Added in-game command to toggle chat bridging setting at run-time.

* .

* Factorio: Document toggle for chat bridging feature.

* (Removed superfluous type annotations.)

* (Removed hard to read regex.)

* Docs/Factorio: Fix display of multiline code snippets.
2022-10-28 00:45:26 +02:00
recklesscoder
924f484be0 Factorio: Add optional filtering for item sends displayed in-game (#1142)
* Factorio: Added feature to filter item sends displayed in-game.

* Factorio: Document item send filter feature.

* Factorio: Fix item send filter for item links.

* (Removed superfluous type annotations.)

* CommonClient: Added is_uninteresting_item_send helper.
2022-10-28 00:07:57 +02:00
Doug Hoskisson
aeb78eaa10 Zillion: map tracker in client (#1136)
* Option RangeWithSpecialMax

* amendment to typing in web options

* compare string with number

* lots of work on zillion

* fix zillion fill logic

* fix a few more issues in zillion fill logic

* can make zillion patch and use it

* put multi items in zillion rom

* work on ZillionClient

* logging and auth in client

* work on sending and receiving items

* implement item_handling flag

* fix locations ids to NuktiServer package

* use rewrite of zri

* cache logic rule data for performance

* use new id maps

* fix some problems with the big recent merge

* ZillionClient: use new context manager for Memory class

* fix ItemClassification for Zillion items
and some debug statements for asserts,
documentation on running scripts for manual testing
type correction in CommonContext

* fix some issues in client, start on docs, put rescue and item ram addresses in slot data

* use new location name system
fix item locations getting out of sync in progression balancing

* zillion client can read slot name from game

* zillion: new item names

* remove extra unneeded import

* newer options (room gen and starting cards)

* update comment in zillion patch

* zillion non static regions

* change some logging, update some comments

* allow ZillionClient to exit in certain situations

* todo note to fix options doc strings

* don't force auto forfeit

* rework validation of floppy requirement and item counts
and fix race condition in generate_output

* reorganize Zillion component structure
with System class

* documentation updates for Zillion

* attempt inno_setup.iss

* remove todo comment for something done

* update comment

* rework item count zillion options
and some small cleanups

* fix location check count

* data package version 1

* Zillion can pass unit tests without rom

* fix freeze if closing ZillionClient while it's waiting for server login

* specify commit hash for zilliandomizer package

* some changes to options validation

* Zillion doors saved on multiworld server

* add missing function in inno_setup
and name of vanilla continues in options

* rework zillion sync task and context

* Apply documentation suggestions from SoldierofOrder

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* update zillion package

* workaround for asyncio udp bug

There is a bug in Python in Windows
https://github.com/python/cpython/issues/91227
that makes it so if I look for RetroArch before it's ready, it breaks the asyncio udp transport system.

As a workaround, we don't look for RetroArch until the user asks for it with /sms

* a few of the smaller suggestions from review

* logic only looks at my locations
instead of all the multiworld locations

* some adjustments from pull request discussion
and some unit tests

* patch webhost changes from pull request discussion

* zillion logic tests

* better vblr test

* test interaction of character rescue items with logic

* move unit tests to new worlds folder

* comment improvements

* fix minor logic issue
and add memory read timeout

* capitalization in option display names
Opa-Opa is a proper noun

* client toggle side panel with /map

* displays map

* fix map transparency

* fix broken launcher

* better way to specify grid container

* start kivy typing

* have a map that updates with item checks

but it breaks other parts of the UI

* fix layout bug

* aspect ratio of image
and some type checking details

* Fix loading of map for compiled builds

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <doughoskisson@novuslabs.com>
Co-authored-by: CaitSith2 <d_good@caitsith2.com>
2022-10-27 02:30:22 -07:00
toasterparty
6134578c60 Overcooked! 2: slightly relax 3-star logic (#1144) 2022-10-27 11:19:48 +02:00
Fabian Dill
b57ca33c31 Logging: more digits for IDs and counts (#1141)
* Logging: we now need 9 digits for IDs

* Logging: we now need {dynamic} digits for IDs

* Logging: we now need {dynamic} digits for counts
2022-10-27 09:18:25 +02:00
Alchav
4b18920819 Early Items option (#1086)
* Early Items option

* Early Items description update

* Change Early Items to dict

* Rewrite early items as extra fill steps

* Move if statement up

* Document early_items

* Move early_items handling before fill_hook

* Apply suggestions from code review

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* Subnautica pre-fill moved to early_items

* Subnautica early items fix

* Remove unit test bug workaround

* beauxq's pr

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2022-10-27 09:00:24 +02:00
Magnemania
700fe8b75e SC2: New Settings, Logic improvements (#1110)
* Switched mission item group to a list comprehension to fix missile shuffle errors

* Logic for reducing mission and item counts

* SC2: Piercing the Shroud/Maw of the Void requirements now DRY

* SC2: Logic for All-In, may need further refinement

* SC2: Additional mission orders and starting locations

* SC2: New Mission Order options for shorter campaigns and smaller item pools

* Using location table for hardcoded starter unit

* SC2: Options to curate random item pool and control early unit placement

* SC2: Proper All-In logic

* SC2: Grid, Mini Grid and Blitz mission orders

* SC2: Required Tactics and Unit Upgrade options, better connected item handling

* SC2: Client compatibility with Grid settings

* SC2: Mission rando now uses world random

* SC2: Alternate final missions, new logic, fixes

* SC2: Handling alternate final missions, identifying final mission on client

* SC2: Minor changes to handle edge-case generation failures

* SC2: Removed invalid type hints for Python 3.8

* Revert "SC2: Removed invalid type hints for Python 3.8"

This reverts commit 7851b9f7a3.

* SC2: Removed invalid type hints for Python 3.8

* SC2: Removed invalid type hints for Python 3.8

* SC2: Removed invalid type hints for Python 3.8

* SC2: Removed invalid type hints for Python 3.8

* SC2: Changed location loop to enumerate

* SC2: Passing category names through slot data

* SC2: Cleaned up unnecessary _create_items method

* SC2: Removed vestigial extra_locations field from MissionInfo

* SC2: Client backwards compatibility

* SC2: Fixed item generation issue where item is present in both locked and unlocked inventories

* SC2: Removed Missile Turret from defense rating on maps without air

* SC2: No logic locations point to same access rule

Co-authored-by: michaelasantiago <michael.alec.santiago@gmail.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2022-10-26 12:24:54 +02:00
PoryGone
d5efc71344 Core: SNI Client Refactor (#1083)
* First Pass removal of game-specific code

* SMW, DKC3, and SM hooked into AutoClient

* All SNES autoclients functional

* Fix ALttP Deathlink

* Don't default to being ALttP, and properly error check ctx.game

* Adjust variable naming

* In response to:
> we should probably document usage somewhere. I'm open to suggestions of where this should be documented.

I think the most valuable documentation for APIs is docstrings and full typing.

about websockets change in imports - from websockets documentation:
> For convenience, many public APIs can be imported from the websockets package. However, this feature is incompatible with static code analysis. It breaks autocompletion in an IDE or type checking with mypy. If you’re using such tools, use the real import paths.

* todo note for python 3.11
typing.NotRequired

* missed staging in previous commit

* added missing death Game States for DeathLink

Co-authored-by: beauxq <beauxq@users.noreply.github.com>
Co-authored-by: lordlou <87331798+lordlou@users.noreply.github.com>
2022-10-25 19:54:43 +02:00
Fabian Dill
6535836e5c Subnautica: don't override plando during pre_fill 2022-10-25 01:35:01 +02:00
lordlou
89d1a80e01 SM: morph first in pool remove (#1134)
* first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions)

* first working single-world randomized SM rom patches

* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions)

* first working single-world randomized SM rom patches

* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* Fixed multiworld support patch not working with VariaRandomizer's

Added stage_fill_hook to set morph first in progitempool
Added back VariaRandomizer's standard patches

* + added missing files from variaRandomizer project

* + added missing variaRandomizer files (custom sprites)

+ started integrating VariaRandomizer options (WIP)

* Some fixes for player and server name display

- fixed player name of 16 characters reading too far in SM client
- fixed 12 bytes SM player name limit (now 16)
- fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO)
- request: temporarly changed default seed names displayed in SM main menu to OWTCH

* Fixed Goal completion not triggering in smClient

* integrated VariaRandomizer's options into AP (WIP)

- startAP is working
- door rando is working
- skillset is working

* - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off")

* skillset are now instanced per player instead of being a singleton class

* RomPatches are now instanced per player instead of being a singleton class

* DoorManager is now instanced per player instead of being a singleton class

* - fixed the last bugs that prevented generation of >1 SM world

* fixed crash when no skillset preset is specified in randoPreset (default to "casual")

* maxDifficulty support and itemsounds removal

- added support for maxDifficulty
- removed itemsounds patch as its always applied from multiworld patch for now

* Fixed bad merge

* Post merge adaptation

* fixed player name length fix that got lost with the merge

* fixed generation with other game type than SM

* added default randoPreset json for SM in playerSettings.yaml

* fixed broken SM client following merge

* beautified json skillset presets

* Fixed ArchipelagoSmClient not building

* Fixed conflict between mutliworld patch and beam_doors_plms patch

- doorsColorsRando now working

* SM generation now outputs APBP

- Fixed paths for patches and presets when frozen

* added missing file and fixed multithreading issue

* temporarily set data_version = 0

* more work

- added support for AP starting items
- fixed client crash with gamemode being None
- patch.py "compatible_version" is now 3

* commited missing asm files

fixed start item reserve breaking game (was using bad write offset when patching)

* Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it).

fixed crash in SMClient when loosing connection to SNI

* fixed No Energy Item missing its ID

fixed Plando

* merge post fixes

* fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color)

* fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses)

* fixed start item x-ray HUD display

* Fixed start items being sent by the server (is all handled in ROM)

Start items are now not removed from itempool anymore
Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though.
Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified

* fixed settings that could be applied to any SM players

* fixed auth to server only using player name (now does as ALTTP to authenticate)

* - fixed End Credits broken text

* added non SM item name display

* added all supported SM options in playerSettings.yaml

* fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region

did some cleaning (mainly reverts on unnecessary core classes

* minor setting fixes and tweaks

- merged Area and lightArea settings
- made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating
- fixed inverted layoutPatch setting

* added option start_inventory_removes_from_pool

fixed option names formatting
fixed lint errors
small code and repo cleanup

* Hopefully fixed ROR2 that could not send any items

* - fixed missing required change to ROR2

* fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum)

* fixed typo with doors_colors_rando

* fixed checksum

* added custom sprites for off-world items (progression or not)

the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu

* - added missing change following upstream merge

- changed patch filename extension from apbp to apm3 so patch can be used with the new client

* added morph placement options: early means local and sphere 1

* fixed failing unit tests

* - fixed broken custom_preset options

* - big cleanup to remove unnecessary or unsupported features

* - more cleanup

* - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips

- small cleanup

* - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch)

* fixed g4_skip patch that can be not applied if hud is enabled

* - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette)

* - updated basepatch to reflect g4_skip removal

- moved more asm files to SMBasepatch project

* - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed)

* fixed wrong path if using built as exe

* - cleaned exposed maxDifficulty options

- removed always enabled Knows

* Merged LttPClient and SMClient into SNIClient

* added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service

* small doc precision

* - added death_link support

- fixed broken Goal Completion
- post merge fix

* - removed now useless presets

* - fixed bad internal mapping with maxDiff

- increases maxDiff if only Bosses is preventing beating the game

* - added support for lowercase custom preset sections (knows, settings and controller)

- fixed controller settings not applying to ROM

* - fixed death loop when dying with Door rando, bomb or speed booster as starting items

- varia's backup save should now be usable (automatically enabled when doing door rando)

* -added docstring for generated yaml

* fixed bad merge

* fixed broken infinity max difficulty

* commented debug prints

* adjusted credits to mark progression speed and difficulty as Non Available

* added support for more than 255 players (will print Archipelago for higher player number)

* fixed missing cleanup

* added support for 65535 different player names in ROM

* fixed generations failing when only bosses are unreachable

* - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish

* fixed failling generations when using 'fun' settings

Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings

* fixed debug logger

* removed unsupported "suits_restriction" option

* fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP)

* - fixed deathlink emptying reserves

- added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves

* - merged death_link and death_link_survive options

* fixed death_link

* added a fallback default starting location instead of failing generation if an invalid one was chosen

* added Nothing and NoEnergy as hint blacklist

added missing NoEnergy as local items and removed it from progression

* removed now unecessary sorting of Morph balls at end of item pool

Its messing with priority locations feature.
2022-10-24 10:28:08 +02:00
beauxq
ad445629bd Zillion: fix unit tests
previous fix was incorrect
2022-10-24 01:28:06 +02:00
Doug Hoskisson
37c5865c0e Core: Options: fix shared default instances (#1130) 2022-10-23 18:28:09 +02:00
Doug Hoskisson
52726139b4 Zillion: support unicode player names (#1131)
* work on unicode and seed verification

* update zilliandomizer

* fix log message
2022-10-23 18:18:05 +02:00
Doug Hoskisson
24105ac249 Tests: fix random failures on Zillion tests (#1128)
* tests: fix random failures on Zillion tests

Normally there's a low probably that the game doesn't require a power-up that it usually requires.
This makes sure it always has that requirement for tests.

* better type narrowing
2022-10-22 03:42:16 +02:00
Alchav
f18df4c1df [Core] Fix priority location handling in accessibility corrections (#1121)
* Fix priority location handling in accessibility corrections

* Don't lock empty locations

* black sliver's suggested change

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-10-22 03:29:20 +02:00
black-sliver
04b6c31076 SoE: update to v042 and balancing changes (#1125)
* SoE: rebalancing and cleanup

* ModuleUpdate: make url detection more generic

* SoE: change item rules to depend on target player difficulty

* SoE: Update to pyevermizer 0.41.0

* adds footknight
* adds location difficulty

* SoE: minor optimization in item rule

if .. in is faster with sets

* SoE: drop support of patch format v3

* SoE: fix some typing and warnings

* SoE: cleanup imports
2022-10-21 23:26:40 +02:00
Fabian Dill
40c3ef35c7 LttP: fix Inverted Big Bomb Shop indirect connection rule 2022-10-21 23:25:52 +02:00
Fabian Dill
28483a6c14 Generate: don't try to include meta or filler weights file as player 2022-10-21 23:25:26 +02:00
ScootyPuffJr1
fa077defe0 [Factorio] Minor fix for typo on setup doc 2022-10-21 08:58:12 +02:00
Chris Wilson
47b4e2782b WebHost: Fix weighted-settings to not save full set of range options to localStorage (#1100) 2022-10-20 20:10:38 -05:00
Doug Hoskisson
265ee7098a New Game: Zillion (#1081)
* Option RangeWithSpecialMax

* amendment to typing in web options

* compare string with number

* lots of work on zillion

* fix zillion fill logic

* fix a few more issues in zillion fill logic

* can make zillion patch and use it

* put multi items in zillion rom

* work on ZillionClient

* logging and auth in client

* work on sending and receiving items

* implement item_handling flag

* fix locations ids to NuktiServer package

* use rewrite of zri

* cache logic rule data for performance

* use new id maps

* fix some problems with the big recent merge

* ZillionClient: use new context manager for Memory class

* fix ItemClassification for Zillion items
and some debug statements for asserts,
documentation on running scripts for manual testing
type correction in CommonContext

* fix some issues in client, start on docs, put rescue and item ram addresses in slot data

* use new location name system
fix item locations getting out of sync in progression balancing

* zillion client can read slot name from game

* zillion: new item names

* remove extra unneeded import

* newer options (room gen and starting cards)

* update comment in zillion patch

* zillion non static regions

* change some logging, update some comments

* allow ZillionClient to exit in certain situations

* todo note to fix options doc strings

* don't force auto forfeit

* rework validation of floppy requirement and item counts
and fix race condition in generate_output

* reorganize Zillion component structure
with System class

* documentation updates for Zillion

* attempt inno_setup.iss

* remove todo comment for something done

* update comment

* rework item count zillion options
and some small cleanups

* fix location check count

* data package version 1

* Zillion can pass unit tests without rom

* fix freeze if closing ZillionClient while it's waiting for server login

* specify commit hash for zilliandomizer package

* some changes to options validation

* Zillion doors saved on multiworld server

* add missing function in inno_setup
and name of vanilla continues in options

* rework zillion sync task and context

* Apply documentation suggestions from SoldierofOrder

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* update zillion package

* workaround for asyncio udp bug

There is a bug in Python in Windows
https://github.com/python/cpython/issues/91227
that makes it so if I look for RetroArch before it's ready, it breaks the asyncio udp transport system.

As a workaround, we don't look for RetroArch until the user asks for it with /sms

* a few of the smaller suggestions from review

* logic only looks at my locations
instead of all the multiworld locations

* some adjustments from pull request discussion
and some unit tests

* patch webhost changes from pull request discussion

* zillion logic tests

* better vblr test

* test interaction of character rescue items with logic

* move unit tests to new worlds folder

* comment improvements

* fix minor logic issue
and add memory read timeout

* capitalization in option display names
Opa-Opa is a proper noun

* redirect zz stdout to debug

* fix option validation bug making unbeatable seeds

* remove line that does nothing

* attach logic cache to world

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <doughoskisson@novuslabs.com>
2022-10-20 19:41:11 +02:00
black-sliver
ed76c13961 Core: assert that items have a single reference (#1075)
* Core: assert that items have a single reference

* Fix duplicate item reference in The Witness

* Ori: fix duplicate item references

* DKC3: fix duplicate item references

* RL: fix duplicate item references

* SA2B: fix duplicate item references

* SMW: fix duplicate item references

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2022-10-20 10:42:33 +02:00
NewSoupVi
40b7e78178 The Witness: More Puzzle Skips, Softlock Fix (#1117)
* Softlock fix: 'Swamp Maze Controls' item also locks maze control other side now

* Increase default & max amount of Puzzle Skips (in response to client side changes to how they work)

* Logically require shortbox lasers to start the elevator
2022-10-20 05:05:36 +02:00
Sunny Bat
1900d9382a Raft: Update rules to account for navigation (#1118) 2022-10-19 08:47:33 +02:00
Doug Hoskisson
f12b73f487 Tests: world test base class (#1116)
* world test base class

* game not instance property

* I would have guessed that this only collected 1.

* game property

* move SoE tests into worlds

* don't force auto world setup
2022-10-18 18:54:41 +02:00
black-sliver
49ae79e5ce Tests: add fill tests for #1109 and #1114 (#1115)
* Test: for fill issues fixed in PR #1109

* Test: for double sweep collect fixed in PR #1114
2022-10-18 10:20:57 +02:00
Fabian Dill
4da6a0bb98 Fill: fix duplicate event pickups 2022-10-18 08:59:51 +02:00
Fabian Dill
af0cfc5a38 Fill: Priority locks when placing and does not swap. (#1099)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-10-18 01:07:06 +02:00
Fabian Dill
1aa3e431c8 LttP: fix ganons tower trash fill deleting items that did not fit (#1113) 2022-10-17 09:52:34 +02:00
Fabian Dill
b533ffb9e8 Locality: rewrite for linear memory consumption, from quadratic (#1091) 2022-10-17 03:22:02 +02:00
Fabian Dill
bb46ee7fc1 WebHost: optimize imports 2022-10-17 01:24:02 +02:00
black-sliver
acf7fda26a Main: Fill: more opportunities for swapping 2022-10-17 00:45:51 +02:00
Alchav
f7fc6fa7aa Core: Fix forbid_important_item_rule with parenthesis (#1107) 2022-10-16 07:32:17 +02:00
Alchav
5e97463bdc [Pokemon R/B] Fix inno_setup mistake (#1105) 2022-10-16 02:53:07 +02:00
Doug Hoskisson
ca9c3d05d6 Docs: information on Retrieved packet (#1101) 2022-10-15 13:44:39 +02:00
espeon65536
51f65f4b9e OoT: ER algorithm improvements (#1103)
* OoT: ER improvements
Include dungeon rewards in itempool to allow for ER improvement
Better validate_world function by checking for multi-entrance incompatibility more efficiently
Fix some generation failures by ensuring all entrances placed with logic
Introduce bias to some interior entrance placement to improve generation rate

* OoT: fix overworld ER spoiler information

* OoT: rewrite dungeon item placement algorithm
in particular, no longer assumes that exactly the number of vanilla keys is present, which lets it place more or fewer items.
2022-10-15 12:39:04 +02:00
recklesscoder
1f01404ca4 WebHost: Fixed scrolling to anchors (#1085)
* WebHost: Modernized anchor code

* WebHost: Fixed scrolling to anchors

* WebHost: Fixed scrolling to anchors when fonts are being loaded

* WebHost: Anchor PR changes requested by LegendaryLinux
2022-10-14 17:09:17 -04:00
Fabian Dill
bbb6ee89cf Hylics 2: add to readme (#1094) 2022-10-14 22:53:49 +02:00
Fabian Dill
0aea1e780f Fill: create minimal excluded location rule only once 2022-10-14 22:53:16 +02:00
Fabian Dill
722b3c5369 Core: make add_rule set if it finds an empty rule (#1093)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-10-14 22:52:45 +02:00
black-sliver
097ac189e4 SoE: add tests ... (#1097)
* SoE: add tests ...

... for goals, bronze axe and bronze spear+

* SoE: fix tests
2022-10-14 19:35:53 +02:00
toasterparty
7f3f886e41 Overcooked! 2: Implementation (#1046)
Overcooked! 2 is a couch co-op arcade game with a very high skill ceiling. It has a small but occult following, and the community craves a reason to keep coming back besides just grinding high scores. as such, this PR represents 3 major milestones in one:

 * The launch of OC2 Modding, a modding framework which is the first public mod for the game beyond simple RAM trainers
 * The launch of OC2 Randomizer
 * The integration of OC2 Randomizer in Archipelago
2022-10-13 19:57:50 +02:00
Alchav
3bd4ef3f3d [Pokémon R/B] Fixes (#1096)
* Prevent legendaries from being shuffled into restless soul encounter

* Prevent Poke Tower 6F wild mons from being same as restless soul

* fix non-deterministic generation
2022-10-13 19:55:21 +02:00
Yussur Mustafa Oraji
6b9073acd7 sm64ex: Update min client version 2022-10-13 11:36:04 +02:00
Jarno
e708bea819 [Sudoku] Added new BK mode game (#910)
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
2022-10-13 07:55:00 +02:00
Trevor L
b014ce082b Hylics 2: Implement new game (#1058) 2022-10-13 07:51:25 +02:00
Alchav
30a4bcbbbe [Pokemon Red and Blue] Initial implementation (#1016) 2022-10-13 07:45:52 +02:00
Alchav
0afb7096de Core: improve sweep_for_events efficiency (#1092) 2022-10-13 05:46:07 +02:00
alwaysintreble
f909576813 core: Generic boss plando handler (#1044)
* fix some blunders i made when implementing this

* move generic functions to core class

* move lttp specific stuff out and split up from_text a bit for more modularity

* slightly optimize from_text call order

* don't make changes on github apparently. reading hard

* Metaclass Magic

* do a check against the base class

* copy paste strikes again

* use option default instead of hardcoded "none". check locations and bosses aren't reused.

* throw dupe location error for lttp

* generic singularity support with a bool

* forgot to enable it for lttp

* better error handling

* PlandoBosses: fix inheritance of singularity

* Tests: PlandoBosses

* fix case insensitive tests

* Tests: cleanup PlandoBosses tests

* f in the chat

* oop

* split location into a different variable

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* pass the list of options as `option_list`

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-10-12 20:28:32 +02:00
lordlou
9f684b3dc0 SMZ3: Shield Upgrade typo fix (#1088)
fixed "Shield Uprade" typo

See lordlou/alttp_sm_combo_randomizer_rom@3da5313
2022-10-11 08:38:00 +02:00
recklesscoder
37a40499fa Docs/sm64ex: address common questions
- Added bolded note that one must use a new file with each new seed
- Added troubleshooting section for when one didn't use a new file
- Worded compilation step 8 more explicitly
- Added link to compilation options list
- Added link to TextClient for using commands
2022-10-10 09:14:13 +02:00
recklesscoder
099c4fca3c Docs: Polished Trigger and Plando guides (#1080)
* Docs: Polished Trigger and Plando guides

* Docs: Trigger/Plando guide polish PR suggestions by SoldierofOrder

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* Docs: More Trigger/Plando guide polish

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
2022-10-10 09:10:01 +02:00
Joethepic
106d630ad7 HK: changes dreamers to skip prog balance (#1077) 2022-10-10 02:26:33 +02:00
alwaysintreble
4c0c93b083 core: allow string defaults in yaml templates (#1051)
* allow string defaults in yaml templates

* have default_converter handle strings

* handle all default values in the yaml

* allow for random range options

* yaml dump dicts

* strip the whities

* rip out the converter

* accidentally stripped the dicts

* goodbye readability
2022-10-10 00:16:59 +02:00
Doug Hoskisson
3cbbf905d1 Docs: how to run web host and generate template yamls (#1071) 2022-10-09 04:20:01 +02:00
SoldierofOrder
414ebf2640 SC2: Add an automated installation process for the maps and mod within SC2Client. (#928) 2022-10-09 04:19:17 +02:00
NewSoupVi
3297be7902 The Witness: Expert & Hints (#1072) 2022-10-09 04:13:52 +02:00
recklesscoder
7b3ef012b9 Factorio: Prevent pipes from breaking on invalid UTF-8 in client (#1078) 2022-10-09 04:10:22 +02:00
black-sliver
af6a72c3c3 AppImage: provide LD_LIBRARY_PATH
this fixes libssl1.1 not being found
2022-10-07 22:24:14 +02:00
recklesscoder
38b7bdfe60 WebHost: Fixed some document titles (#1063) 2022-10-06 18:01:07 -04:00
Gertimoshka
4c266e6eff hostRoom.css Changes (#957)
* hostRoom.css Changes

Makes the console be a scrollable object, for easier use with commands

* Update hostRoom.css

* Requested Change

Requested Change
2022-10-06 17:53:20 -04:00
Fabian Dill
8a6c9ff4b8 WebHost: clear yaml template folder before populating it 2022-10-03 08:48:53 +02:00
Doug Hoskisson
fdd7ffb089 Core: move output file name logic into core (#1066)
* move output file name logic into core

I see the same logic with small variations in each different world implementation.
It seems to me, it would be better in the core to keep it consistent.

* missed a few

* remove review comment

* + smw

* double quote strings

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* revert change to DS3 output file name

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-10-02 16:53:18 +02:00
black-sliver
b8e467fbb8 ModuleUpdate: skip disabled/hidden folders (#1070)
* ModuleUpdate: skip non-worlds

* ModuleUpdate: don't skip _* folders

- _* folders may be used for libraries
- this means to properly disable a world, it has to be renamed with a preceding `.`
2022-10-01 17:38:39 +02:00
Fabian Dill
411cd51a92 SC2: dynamically create Beat <mission_name> Events, preventing copy-paste errors (#1023) 2022-10-01 16:22:24 +02:00
Fabian Dill
e9e15e854d SC2: make apworld compatible (#1024)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-10-01 15:24:05 +02:00
black-sliver
4943d26160 APWorld: make it behave more like a regular world
- set sys.modules so it can be imported with worlds.*
- overwrite __package__ so it can reference ..generic
- fix deprecation warning
2022-10-01 11:51:42 +02:00
Fabian Dill
060a04700d Core: allow generic access to indirect_connections (#1056) 2022-09-30 04:58:19 +02:00
Fabian Dill
61e39f355d Core remove legacy patch (#1047)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-30 00:36:30 +02:00
Yussur Mustafa Oraji
8ab0b410c3 sm64ex: Document new connection status notifications 2022-09-29 23:56:46 +02:00
Fabian Dill
d897aaade2 Docs: Ensure Discord links are permanent. (#1064) 2022-09-29 23:15:12 +02:00
black-sliver
0191df88d7 Doc: network protocol: clarify want_reply 2022-09-29 21:15:34 +02:00
recklesscoder
bee1fd9b5a Subnautica: Updated Setup Guide (#1062)
- Added sections for console commands and known issues.
- Updated "Resuming" section to reflect current functionality.
- Removed implication that one might have to create the QMods folder. (If it's missing, then you've already messed up step 1.)
- Renamed "Connect Menu" to "connect form" to be less confusing. Generally fixed word capitalization to conform to standard English. Minor wording changes.
2022-09-29 21:04:04 +02:00
Jarno
dd7d3a02a4 WebHost: Fixed Oculus Ring from showing up on tracker (#1065) 2022-09-29 20:18:21 +02:00
PoryGone
13edfa60be Super Mario World: Implement New Game (#1045) 2022-09-29 20:16:59 +02:00
Alchav
885c8d3fcc Fix minimal accessibility failures (#726) 2022-09-29 19:59:57 +02:00
black-sliver
e6a4925f0c Doc: update apclientpp to header-only (#1054) 2022-09-29 00:09:04 +02:00
Doug Hoskisson
c96b6d7b95 Core: some typing and docs in various parts of the interface (#1060)
* some typing and docs in various parts of the interface

* fix whitespace in docstring

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* suggested changes from discussion

* remove redundant import

* adjust type for json messages

* for options module detection:
 module.lower().endswith("options")

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-28 23:54:10 +02:00
alwaysintreble
8bc8b412a3 Core: fix unweighted options for meta files (#1053) 2022-09-28 23:02:42 +02:00
PoryGone
b4b9ff5d82 Docs: Update snes9x Links (#1048) 2022-09-27 13:26:33 +02:00
black-sliver
b21b5cceb8 Doc, SoE: Logic mixin: no underscore for public members (#1049)
* Doc: logic mixin, drop underscore, clarify

conventionally, we added a leading underscore to logic mixins' function
names. This is noisy in the warning section of IDEs. Leading underscores
should only be used for private/protected functions.

In addition, the use of self.world and/or requirement to (no) pass in stuff
was not made clear earlier.

* SoE: fix _ warnings for logic mixin
2022-09-25 18:00:22 +02:00
1258 changed files with 177740 additions and 60830 deletions

View File

@@ -0,0 +1,80 @@
name: Analyze modified files
on:
pull_request:
paths:
- "**.py"
push:
paths:
- "**.py"
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
BEFORE: ${{ github.event.before }}
AFTER: ${{ github.event.after }}
jobs:
flake8-or-mypy:
strategy:
fail-fast: false
matrix:
task: [flake8, mypy]
name: ${{ matrix.task }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: "Determine modified files (pull_request)"
if: github.event_name == 'pull_request'
run: |
git fetch origin $BASE $HEAD
DIFF=$(git diff --diff-filter=d --name-only $BASE...$HEAD -- "*.py")
echo "modified files:"
echo "$DIFF"
echo "diff=${DIFF//$'\n'/$' '}" >> $GITHUB_ENV
- name: "Determine modified files (push)"
if: github.event_name == 'push' && github.event.before != '0000000000000000000000000000000000000000'
run: |
git fetch origin $BEFORE $AFTER
DIFF=$(git diff --diff-filter=d --name-only $BEFORE..$AFTER -- "*.py")
echo "modified files:"
echo "$DIFF"
echo "diff=${DIFF//$'\n'/$' '}" >> $GITHUB_ENV
- name: "Treat all files as modified (new branch)"
if: github.event_name == 'push' && github.event.before == '0000000000000000000000000000000000000000'
run: |
echo "diff=." >> $GITHUB_ENV
- uses: actions/setup-python@v4
if: env.diff != ''
with:
python-version: 3.8
- name: "Install dependencies"
if: env.diff != ''
run: |
python -m pip install --upgrade pip ${{ matrix.task }}
python ModuleUpdate.py --append "WebHostLib/requirements.txt" --force --yes
- name: "flake8: Stop the build if there are Python syntax errors or undefined names"
continue-on-error: false
if: env.diff != '' && matrix.task == 'flake8'
run: |
flake8 --count --select=E9,F63,F7,F82 --show-source --statistics ${{ env.diff }}
- name: "flake8: Lint modified files"
continue-on-error: true
if: env.diff != '' && matrix.task == 'flake8'
run: |
flake8 --count --max-complexity=10 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }}
- name: "mypy: Type check modified files"
continue-on-error: true
if: env.diff != '' && matrix.task == 'mypy'
run: |
mypy --follow-imports=silent --install-types --non-interactive --strict ${{ env.diff }}

View File

@@ -2,10 +2,20 @@
name: Build
on: workflow_dispatch
on:
push:
paths:
- '.github/workflows/build.yml'
- 'setup.py'
- 'requirements.txt'
pull_request:
paths:
- '.github/workflows/build.yml'
- 'setup.py'
- 'requirements.txt'
workflow_dispatch:
env:
SNI_VERSION: v0.0.84
ENEMIZER_VERSION: 7.1
APPIMAGETOOL_VERSION: 13
@@ -15,22 +25,19 @@ jobs:
build-win-py38: # RCs will still be built and signed by hand
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Install python
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: Download run-time dependencies
run: |
Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/${Env:SNI_VERSION}/sni-${Env:SNI_VERSION}-windows-amd64.zip -OutFile sni.zip
Expand-Archive -Path sni.zip -DestinationPath SNI -Force
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
- name: Build
run: |
python -m pip install --upgrade pip setuptools
pip install -r requirements.txt
python setup.py build --yes
python -m pip install --upgrade pip
python setup.py build_exe --yes
$NAME="$(ls build)".Split('.',2)[1]
$ZIP_NAME="Archipelago_$NAME.7z"
echo "ZIP_NAME=$ZIP_NAME" >> $Env:GITHUB_ENV
@@ -39,24 +46,24 @@ jobs:
Rename-Item exe.$NAME Archipelago
7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago
- name: Store 7z
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: ${{ env.ZIP_NAME }}
path: dist/${{ env.ZIP_NAME }}
retention-days: 7 # keep for 7 days, should be enough
build-ubuntu1804:
runs-on: ubuntu-18.04
build-ubuntu2004:
runs-on: ubuntu-20.04
steps:
# - copy code below to release.yml -
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Install base dependencies
run: |
sudo apt update
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
- name: Get a recent python
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install build-time dependencies
@@ -69,20 +76,16 @@ jobs:
chmod a+rx appimagetool
- name: Download run-time dependencies
run: |
wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz
tar xf sni-*.tar.xz
rm sni-*.tar.xz
mv sni-* SNI
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
- name: Build
run: |
# pygobject is an optional dependency for kivy that's not in requirements
"${{ env.PYTHON }}" -m pip install --upgrade pip virtualenv PyGObject setuptools
# charset-normalizer was somehow incomplete in the github runner
"${{ env.PYTHON }}" -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python setup.py build --yes bdist_appimage --yes
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer
python setup.py build_exe --yes bdist_appimage --yes
echo -e "setup.py build output:\n `ls build`"
echo -e "setup.py dist output:\n `ls dist`"
cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd ..
@@ -91,14 +94,18 @@ jobs:
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
# - copy code above to release.yml -
- name: Build Again
run: |
source venv/bin/activate
python setup.py build_exe --yes
- name: Store AppImage
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: ${{ env.APPIMAGE_NAME }}
path: dist/${{ env.APPIMAGE_NAME }}
retention-days: 7
- name: Store .tar.gz
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: ${{ env.TAR_NAME }}
path: dist/${{ env.TAR_NAME }}

View File

@@ -14,9 +14,17 @@ name: "CodeQL"
on:
push:
branches: [ main ]
paths:
- '**.py'
- '**.js'
- '.github/workflows/codeql-analysis.yml'
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
paths:
- '**.py'
- '**.js'
- '.github/workflows/codeql-analysis.yml'
schedule:
- cron: '44 8 * * 1'
@@ -35,11 +43,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -50,7 +58,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -64,4 +72,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

View File

@@ -1,29 +0,0 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: lint
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.9
- name: Install dependencies
run: |
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: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

View File

@@ -8,7 +8,6 @@ on:
- '*.*.*'
env:
SNI_VERSION: v0.0.84
ENEMIZER_VERSION: 7.1
APPIMAGETOOL_VERSION: 13
@@ -30,20 +29,20 @@ jobs:
# build-release-windows: # this is done by hand because of signing
# build-release-macos: # LF volunteer
build-release-ubuntu1804:
runs-on: ubuntu-18.04
build-release-ubuntu2004:
runs-on: ubuntu-20.04
steps:
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
# - code below copied from build.yml -
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Install base dependencies
run: |
sudo apt update
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
- name: Get a recent python
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install build-time dependencies
@@ -56,20 +55,16 @@ jobs:
chmod a+rx appimagetool
- name: Download run-time dependencies
run: |
wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz
tar xf sni-*.tar.xz
rm sni-*.tar.xz
mv sni-* SNI
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
- name: Build
run: |
# pygobject is an optional dependency for kivy that's not in requirements
"${{ env.PYTHON }}" -m pip install --upgrade pip virtualenv PyGObject setuptools
# charset-normalizer was somehow incomplete in the github runner
"${{ env.PYTHON }}" -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python setup.py build --yes bdist_appimage --yes
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer
python setup.py build_exe --yes bdist_appimage --yes
echo -e "setup.py build output:\n `ls build`"
echo -e "setup.py dist output:\n `ls dist`"
cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd ..

View File

@@ -3,7 +3,25 @@
name: unittests
on: [push, pull_request]
on:
push:
paths:
- '**'
- '!docs/**'
- '!setup.py'
- '!*.iss'
- '!.gitignore'
- '!.github/workflows/**'
- '.github/workflows/unittests.yml'
pull_request:
paths:
- '**'
- '!docs/**'
- '!setup.py'
- '!*.iss'
- '!.gitignore'
- '!.github/workflows/**'
- '.github/workflows/unittests.yml'
jobs:
build:
@@ -23,18 +41,20 @@ jobs:
os: windows-latest
- python: {version: '3.10'} # current
os: windows-latest
- python: {version: '3.10'} # current
os: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python.version }}
uses: actions/setup-python@v1
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python.version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip wheel
pip install flake8 pytest pytest-subtests
python -m pip install --upgrade pip
pip install pytest pytest-subtests
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
- name: Unittests
run: |
pytest test
pytest

20
.gitignore vendored
View File

@@ -4,15 +4,21 @@
*_Spoiler.txt
*.bmbp
*.apbp
*.apl2ac
*.apm3
*.apmc
*.apz5
*.aptloz
*.pyc
*.pyd
*.sfc
*.z64
*.n64
*.nes
*.sms
*.gb
*.gbc
*.gba
*.wixobj
*.lck
*.db3
@@ -20,7 +26,9 @@
*multisave
*.archipelago
*.apsave
*.BIN
setups
build
bundle/components.wxs
dist
@@ -44,7 +52,9 @@ Output Logs/
/freeze_requirements.txt
/Archipelago.zip
/setup.ini
/installdelete.iss
/data/user.kv
/datapackage
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -125,12 +135,14 @@ ipython_config.py
# Environments
.env
.venv
.venv*
env/
venv/
ENV/
env.bak/
venv.bak/
.code-workspace
shell.nix
# Spyder project settings
.spyderproject
@@ -160,10 +172,14 @@ cython_debug/
jdk*/
minecraft*/
minecraft_versions.json
!worlds/minecraft/
# pyenv
.python-version
#undertale stuff
/Undertale/
# OS General Files
.DS_Store
.AppleDouble

516
AdventureClient.py Normal file
View File

@@ -0,0 +1,516 @@
import asyncio
import hashlib
import json
import time
import os
import bsdiff4
import subprocess
import zipfile
from asyncio import StreamReader, StreamWriter, CancelledError
from typing import List
import Utils
from NetUtils import ClientStatus
from Utils import async_start
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
get_base_parser
from worlds.adventure import AdventureDeltaPatch
from worlds.adventure.Locations import base_location_id
from worlds.adventure.Rom import AdventureForeignItemInfo, AdventureAutoCollectLocation, BatNoTouchLocation
from worlds.adventure.Items import base_adventure_item_id, standard_item_max, item_table
from worlds.adventure.Offsets import static_item_element_size, connector_port_offset
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = \
"Connection timing out. Please restart your emulator, then restart connector_adventure.lua"
CONNECTION_REFUSED_STATUS = \
"Connection Refused. Please start your emulator and make sure connector_adventure.lua is running"
CONNECTION_RESET_STATUS = \
"Connection was reset. Please restart your emulator, then restart connector_adventure.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
SCRIPT_VERSION = 1
class AdventureCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx: CommonContext):
super().__init__(ctx)
def _cmd_2600(self):
"""Check 2600 Connection State"""
if isinstance(self.ctx, AdventureContext):
logger.info(f"2600 Status: {self.ctx.atari_status}")
def _cmd_aconnect(self):
"""Discard current atari 2600 connection state"""
if isinstance(self.ctx, AdventureContext):
self.ctx.atari_sync_task.cancel()
class AdventureContext(CommonContext):
command_processor = AdventureCommandProcessor
game = 'Adventure'
lua_connector_port: int = 17242
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.freeincarnates_used: int = -1
self.freeincarnate_pending: int = 0
self.foreign_items: [AdventureForeignItemInfo] = []
self.autocollect_items: [AdventureAutoCollectLocation] = []
self.atari_streams: (StreamReader, StreamWriter) = None
self.atari_sync_task = None
self.messages = {}
self.locations_array = None
self.atari_status = CONNECTION_INITIAL_STATUS
self.awaiting_rom = False
self.display_msgs = True
self.deathlink_pending = False
self.set_deathlink = False
self.client_compatibility_mode = 0
self.items_handling = 0b111
self.checked_locations_sent: bool = False
self.port_offset = 0
self.bat_no_touch_locations: [BatNoTouchLocation] = []
self.local_item_locations = {}
self.dragon_speed_info = {}
options = Utils.get_options()
self.display_msgs = options["adventure_options"]["display_msgs"]
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(AdventureContext, self).server_auth(password_requested)
if not self.auth:
self.auth = self.player_name
if not self.auth:
self.awaiting_rom = True
logger.info('Awaiting connection to adventure_connector to get Player information')
return
await self.send_connect()
def _set_message(self, msg: str, msg_id: int):
if self.display_msgs:
self.messages[(time.time(), msg_id)] = msg
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
self.locations_array = None
if Utils.get_options()["adventure_options"].get("death_link", False):
self.set_deathlink = True
async_start(self.get_freeincarnates_used())
elif cmd == "RoomInfo":
self.seed_name = args['seed_name']
elif cmd == 'Print':
msg = args['text']
if ': !' not in msg:
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "ReceivedItems":
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "Retrieved":
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
if self.freeincarnates_used is None:
self.freeincarnates_used = 0
self.freeincarnates_used += self.freeincarnate_pending
self.send_pending_freeincarnates()
elif cmd == "SetReply":
if args["key"] == f"adventure_{self.auth}_freeincarnates_used":
self.freeincarnates_used = args["value"]
if self.freeincarnates_used is None:
self.freeincarnates_used = 0
self.freeincarnates_used += self.freeincarnate_pending
self.send_pending_freeincarnates()
def on_deathlink(self, data: dict):
self.deathlink_pending = True
super().on_deathlink(data)
def run_gui(self):
from kvui import GameManager
class AdventureManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Adventure Client"
self.ui = AdventureManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def get_freeincarnates_used(self):
if self.server and not self.server.socket.closed:
await self.send_msgs([{"cmd": "SetNotify", "keys": [f"adventure_{self.auth}_freeincarnates_used"]}])
await self.send_msgs([{"cmd": "Get", "keys": [f"adventure_{self.auth}_freeincarnates_used"]}])
def send_pending_freeincarnates(self):
if self.freeincarnate_pending > 0:
async_start(self.send_pending_freeincarnates_impl(self.freeincarnate_pending))
self.freeincarnate_pending = 0
async def send_pending_freeincarnates_impl(self, send_val: int) -> None:
await self.send_msgs([{"cmd": "Set", "key": f"adventure_{self.auth}_freeincarnates_used",
"default": 0, "want_reply": False,
"operations": [{"operation": "add", "value": send_val}]}])
async def used_freeincarnate(self) -> None:
if self.server and not self.server.socket.closed:
await self.send_msgs([{"cmd": "Set", "key": f"adventure_{self.auth}_freeincarnates_used",
"default": 0, "want_reply": True,
"operations": [{"operation": "add", "value": 1}]}])
else:
self.freeincarnate_pending = self.freeincarnate_pending + 1
def convert_item_id(ap_item_id: int):
static_item_index = ap_item_id - base_adventure_item_id
return static_item_index * static_item_element_size
def get_payload(ctx: AdventureContext):
current_time = time.time()
items = []
dragon_speed_update = {}
diff_a_locked = ctx.diff_a_mode > 0
diff_b_locked = ctx.diff_b_mode > 0
freeincarnate_count = 0
for item in ctx.items_received:
item_id_str = str(item.item)
if base_adventure_item_id < item.item <= standard_item_max:
items.append(convert_item_id(item.item))
elif item_id_str in ctx.dragon_speed_info:
if item.item in dragon_speed_update:
last_index = len(ctx.dragon_speed_info[item_id_str]) - 1
dragon_speed_update[item.item] = ctx.dragon_speed_info[item_id_str][last_index]
else:
dragon_speed_update[item.item] = ctx.dragon_speed_info[item_id_str][0]
elif item.item == item_table["Left Difficulty Switch"].id:
diff_a_locked = False
elif item.item == item_table["Right Difficulty Switch"].id:
diff_b_locked = False
elif item.item == item_table["Freeincarnate"].id:
freeincarnate_count = freeincarnate_count + 1
freeincarnates_available = 0
if ctx.freeincarnates_used >= 0:
freeincarnates_available = freeincarnate_count - (ctx.freeincarnates_used + ctx.freeincarnate_pending)
ret = json.dumps(
{
"items": items,
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
if key[0] > current_time - 10},
"deathlink": ctx.deathlink_pending,
"dragon_speeds": dragon_speed_update,
"difficulty_a_locked": diff_a_locked,
"difficulty_b_locked": diff_b_locked,
"freeincarnates_available": freeincarnates_available,
"bat_logic": ctx.bat_logic
}
)
ctx.deathlink_pending = False
return ret
async def parse_locations(data: List, ctx: AdventureContext):
locations = data
# for loc_name, loc_data in location_table.items():
# if flags["EventFlag"][280] & 1 and not ctx.finished_game:
# await ctx.send_msgs([
# {"cmd": "StatusUpdate",
# "status": 30}
# ])
# ctx.finished_game = True
if locations == ctx.locations_array:
return
ctx.locations_array = locations
if locations is not None:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}])
def send_ap_foreign_items(adventure_context):
foreign_item_json_list = []
autocollect_item_json_list = []
bat_no_touch_locations_json_list = []
for fi in adventure_context.foreign_items:
foreign_item_json_list.append(fi.get_dict())
for fi in adventure_context.autocollect_items:
autocollect_item_json_list.append(fi.get_dict())
for ntl in adventure_context.bat_no_touch_locations:
bat_no_touch_locations_json_list.append(ntl.get_dict())
payload = json.dumps(
{
"foreign_items": foreign_item_json_list,
"autocollect_items": autocollect_item_json_list,
"local_item_locations": adventure_context.local_item_locations,
"bat_no_touch_locations": bat_no_touch_locations_json_list
}
)
print("sending foreign items")
msg = payload.encode()
(reader, writer) = adventure_context.atari_streams
writer.write(msg)
writer.write(b'\n')
def send_checked_locations_if_needed(adventure_context):
if not adventure_context.checked_locations_sent and adventure_context.checked_locations is not None:
if len(adventure_context.checked_locations) == 0:
return
checked_short_ids = []
for location in adventure_context.checked_locations:
checked_short_ids.append(location - base_location_id)
print("Sending checked locations")
payload = json.dumps(
{
"checked_locations": checked_short_ids,
}
)
msg = payload.encode()
(reader, writer) = adventure_context.atari_streams
writer.write(msg)
writer.write(b'\n')
adventure_context.checked_locations_sent = True
async def atari_sync_task(ctx: AdventureContext):
logger.info("Starting Atari 2600 connector. Use /2600 for status information")
while not ctx.exit_event.is_set():
try:
error_status = None
if ctx.atari_streams:
(reader, writer) = ctx.atari_streams
msg = get_payload(ctx).encode()
writer.write(msg)
writer.write(b'\n')
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
# Data will return a dict with 1+ fields
# 1. A keepalive response of the Players Name (always)
# 2. romhash field with sha256 hash of the ROM memory region
# 3. locations, messages, and deathLink
# 4. freeincarnate, to indicate a freeincarnate was used
data = await asyncio.wait_for(reader.readline(), timeout=5)
data_decoded = json.loads(data.decode())
if 'scriptVersion' not in data_decoded or data_decoded['scriptVersion'] != SCRIPT_VERSION:
msg = "You are connecting with an incompatible Lua script version. Ensure your connector " \
"Lua and AdventureClient are from the same Archipelago installation."
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
if ctx.seed_name and bytes(ctx.seed_name, encoding='ASCII') != ctx.seed_name_from_data:
msg = "The server is running a different multiworld than your client is. " \
"(invalid seed_name)"
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
if 'romhash' in data_decoded:
if ctx.rom_hash.upper() != data_decoded['romhash'].upper():
msg = "The rom hash does not match the client rom hash data"
print("got " + data_decoded['romhash'])
print("expected " + str(ctx.rom_hash))
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
if ctx.auth is None:
ctx.auth = ctx.player_name
if ctx.awaiting_rom:
await ctx.server_auth(False)
if 'locations' in data_decoded and ctx.game and ctx.atari_status == CONNECTION_CONNECTED_STATUS \
and not error_status and ctx.auth:
# Not just a keep alive ping, parse
async_start(parse_locations(data_decoded['locations'], ctx))
if 'deathLink' in data_decoded and data_decoded['deathLink'] > 0 and 'DeathLink' in ctx.tags:
dragon_name = "a dragon"
if data_decoded['deathLink'] == 1:
dragon_name = "Rhindle"
elif data_decoded['deathLink'] == 2:
dragon_name = "Yorgle"
elif data_decoded['deathLink'] == 3:
dragon_name = "Grundle"
print (ctx.auth + " has been eaten by " + dragon_name )
await ctx.send_death(ctx.auth + " has been eaten by " + dragon_name)
# TODO - also if player reincarnates with a dragon onscreen ' dies to avoid being eaten by '
if 'victory' in data_decoded and not ctx.finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if 'freeincarnate' in data_decoded:
await ctx.used_freeincarnate()
if ctx.set_deathlink:
await ctx.update_death_link(True)
send_checked_locations_if_needed(ctx)
except asyncio.TimeoutError:
logger.debug("Read Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.atari_streams = None
except ConnectionResetError as e:
logger.debug("Read failed due to Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.atari_streams = None
except TimeoutError:
logger.debug("Connection Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.atari_streams = None
except ConnectionResetError:
logger.debug("Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.atari_streams = None
except CancelledError:
logger.debug("Connection Cancelled, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.atari_streams = None
pass
except Exception as e:
print("unknown exception " + e)
raise
if ctx.atari_status == CONNECTION_TENTATIVE_STATUS:
if not error_status:
logger.info("Successfully Connected to 2600")
ctx.atari_status = CONNECTION_CONNECTED_STATUS
ctx.checked_locations_sent = False
send_ap_foreign_items(ctx)
send_checked_locations_if_needed(ctx)
else:
ctx.atari_status = f"Was tentatively connected but error occurred: {error_status}"
elif error_status:
ctx.atari_status = error_status
logger.info("Lost connection to 2600 and attempting to reconnect. Use /2600 for status updates")
else:
try:
port = ctx.lua_connector_port + ctx.port_offset
logger.debug(f"Attempting to connect to 2600 on port {port}")
print(f"Attempting to connect to 2600 on port {port}")
ctx.atari_streams = await asyncio.wait_for(
asyncio.open_connection("localhost",
port),
timeout=10)
ctx.atari_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
ctx.atari_status = CONNECTION_TIMING_OUT_STATUS
continue
except ConnectionRefusedError:
logger.debug("Connection Refused, Trying Again")
ctx.atari_status = CONNECTION_REFUSED_STATUS
continue
except CancelledError:
pass
except CancelledError:
pass
print("exiting atari sync task")
async def run_game(romfile):
auto_start = Utils.get_options()["adventure_options"].get("rom_start", True)
rom_args = Utils.get_options()["adventure_options"].get("rom_args")
if auto_start is True:
import webbrowser
webbrowser.open(romfile)
elif os.path.isfile(auto_start):
open_args = [auto_start, romfile]
if rom_args is not None:
open_args.insert(1, rom_args)
subprocess.Popen(open_args,
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
async def patch_and_run_game(patch_file, ctx):
base_name = os.path.splitext(patch_file)[0]
comp_path = base_name + '.a26'
try:
base_rom = AdventureDeltaPatch.get_source_data()
except Exception as msg:
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
with open(Utils.local_path("data", "adventure_basepatch.bsdiff4"), "rb") as file:
basepatch = bytes(file.read())
base_patched_rom_data = bsdiff4.patch(base_rom, basepatch)
with zipfile.ZipFile(patch_file, 'r') as patch_archive:
if not AdventureDeltaPatch.check_version(patch_archive):
logger.error("apadvn version doesn't match this client. Make sure your generator and client are the same")
raise Exception("apadvn version doesn't match this client.")
ctx.foreign_items = AdventureDeltaPatch.read_foreign_items(patch_archive)
ctx.autocollect_items = AdventureDeltaPatch.read_autocollect_items(patch_archive)
ctx.local_item_locations = AdventureDeltaPatch.read_local_item_locations(patch_archive)
ctx.dragon_speed_info = AdventureDeltaPatch.read_dragon_speed_info(patch_archive)
ctx.seed_name_from_data, ctx.player_name = AdventureDeltaPatch.read_rom_info(patch_archive)
ctx.diff_a_mode, ctx.diff_b_mode = AdventureDeltaPatch.read_difficulty_switch_info(patch_archive)
ctx.bat_logic = AdventureDeltaPatch.read_bat_logic(patch_archive)
ctx.bat_no_touch_locations = AdventureDeltaPatch.read_bat_no_touch(patch_archive)
ctx.rom_deltas = AdventureDeltaPatch.read_rom_deltas(patch_archive)
ctx.auth = ctx.player_name
patched_rom_data = AdventureDeltaPatch.apply_rom_deltas(base_patched_rom_data, ctx.rom_deltas)
rom_hash = hashlib.sha256()
rom_hash.update(patched_rom_data)
ctx.rom_hash = rom_hash.hexdigest()
ctx.port_offset = patched_rom_data[connector_port_offset]
with open(comp_path, "wb") as patched_rom_file:
patched_rom_file.write(patched_rom_data)
async_start(run_game(comp_path))
if __name__ == '__main__':
Utils.init_logging("AdventureClient")
async def main():
parser = get_base_parser()
parser.add_argument('patch_file', default="", type=str, nargs="?",
help='Path to an ADVNTURE.BIN rom file')
parser.add_argument('port', default=17242, type=int, nargs="?",
help='port for adventure_connector connection')
args = parser.parse_args()
ctx = AdventureContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
ctx.atari_sync_task = asyncio.create_task(atari_sync_task(ctx), name="Adventure Sync")
if args.patch_file:
ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower()
if ext == "apadvn":
logger.info("apadvn file supplied, beginning patching process...")
async_start(patch_and_run_game(args.patch_file, ctx))
else:
logger.warning(f"Unknown patch file extension {ext}")
if args.port is int:
ctx.lua_connector_port = args.port
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.atari_sync_task:
await ctx.atari_sync_task
print("finished atari_sync_task (main)")
import colorama
colorama.init()
asyncio.run(main())
colorama.deinit()

File diff suppressed because it is too large Load Diff

View File

@@ -20,9 +20,13 @@ if __name__ == "__main__":
from MultiServer import CommandProcessor
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \
ClientStatus, Permission, NetworkSlot, RawJSONtoTextParser
from Utils import Version, stream_input
from Utils import Version, stream_input, async_start
from worlds import network_data_package, AutoWorldRegister
import os
import ssl
if typing.TYPE_CHECKING:
import kvui
logger = logging.getLogger("Client")
@@ -30,6 +34,12 @@ logger = logging.getLogger("Client")
gui_enabled = not sys.stdout or "--nogui" not in sys.argv
@Utils.cache_argsless
def get_ssl_context():
import certifi
return ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
class ClientCommandProcessor(CommandProcessor):
def __init__(self, ctx: CommonContext):
self.ctx = ctx
@@ -44,33 +54,38 @@ class ClientCommandProcessor(CommandProcessor):
def _cmd_connect(self, address: str = "") -> bool:
"""Connect to a MultiWorld Server"""
self.ctx.server_address = None
self.ctx.username = None
asyncio.create_task(self.ctx.connect(address if address else None), name="connecting")
if address:
self.ctx.server_address = None
self.ctx.username = None
elif not self.ctx.server_address:
self.output("Please specify an address.")
return False
async_start(self.ctx.connect(address if address else None), name="connecting")
return True
def _cmd_disconnect(self) -> bool:
"""Disconnect from a MultiWorld Server"""
self.ctx.server_address = None
self.ctx.username = None
asyncio.create_task(self.ctx.disconnect(), name="disconnecting")
async_start(self.ctx.disconnect(), name="disconnecting")
return True
def _cmd_received(self) -> bool:
"""List all received items"""
logger.info(f'{len(self.ctx.items_received)} received items:')
self.output(f'{len(self.ctx.items_received)} received items:')
for index, item in enumerate(self.ctx.items_received, 1):
self.output(f"{self.ctx.item_names[item.item]} from {self.ctx.player_names[item.player]}")
return True
def _cmd_missing(self) -> bool:
"""List all missing location checks, from your local game state"""
def _cmd_missing(self, filter_text = "") -> bool:
"""List all missing location checks, from your local game state.
Can be given text, which will be used as filter."""
if not self.ctx.game:
self.output("No game set, cannot determine missing checks.")
return False
count = 0
checked_count = 0
for location, location_id in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id.items():
if filter_text and filter_text not in location:
continue
if location_id < 0:
continue
if location_id not in self.ctx.locations_checked:
@@ -91,12 +106,18 @@ class ClientCommandProcessor(CommandProcessor):
def _cmd_items(self):
"""List all item names for the currently running game."""
if not self.ctx.game:
self.output("No game set, cannot determine existing items.")
return False
self.output(f"Item Names for {self.ctx.game}")
for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id:
self.output(item_name)
def _cmd_locations(self):
"""List all location names for the currently running game."""
if not self.ctx.game:
self.output("No game set, cannot determine existing locations.")
return False
self.output(f"Location Names for {self.ctx.game}")
for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id:
self.output(location_name)
@@ -110,12 +131,12 @@ class ClientCommandProcessor(CommandProcessor):
else:
state = ClientStatus.CLIENT_CONNECTED
self.output("Unreadied.")
asyncio.create_task(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")
async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")
def default(self, raw: str):
raw = self.ctx.on_user_say(raw)
if raw:
asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say")
async_start(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say")
class CommonContext:
@@ -123,8 +144,9 @@ class CommonContext:
tags: typing.Set[str] = {"AP"}
game: typing.Optional[str] = None
items_handling: typing.Optional[int] = None
want_slot_data: bool = True # should slot_data be retrieved via Connect
# datapackage
# data package
# Contents in flux until connection to server is made, to download correct data for this multiworld.
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
@@ -132,38 +154,54 @@ class CommonContext:
# defaults
starting_reconnect_delay: int = 5
current_reconnect_delay: int = starting_reconnect_delay
command_processor: type(CommandProcessor) = ClientCommandProcessor
command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor
ui = None
ui_task: typing.Optional[asyncio.Task] = None
input_task: typing.Optional[asyncio.Task] = None
keep_alive_task: typing.Optional[asyncio.Task] = None
server_task: typing.Optional[asyncio.Task] = None
ui_task: typing.Optional["asyncio.Task[None]"] = None
input_task: typing.Optional["asyncio.Task[None]"] = None
keep_alive_task: typing.Optional["asyncio.Task[None]"] = None
server_task: typing.Optional["asyncio.Task[None]"] = None
autoreconnect_task: typing.Optional["asyncio.Task[None]"] = None
disconnected_intentionally: bool = False
server: typing.Optional[Endpoint] = None
server_version: Version = Version(0, 0, 0)
current_energy_link_value: int = 0 # to display in UI, gets set by server
generator_version: Version = Version(0, 0, 0)
current_energy_link_value: typing.Optional[int] = None # to display in UI, gets set by server
last_death_link: float = time.time() # last send/received death link on AP layer
# remaining type info
slot_info: typing.Dict[int, NetworkSlot]
server_address: str
server_address: typing.Optional[str]
password: typing.Optional[str]
hint_cost: typing.Optional[int]
hint_points: typing.Optional[int]
player_names: typing.Dict[int, str]
finished_game: bool
ready: bool
auth: typing.Optional[str]
seed_name: typing.Optional[str]
# locations
locations_checked: typing.Set[int] # local state
locations_scouted: typing.Set[int]
items_received: typing.List[NetworkItem]
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]
# data storage
stored_data: typing.Dict[str, typing.Any]
stored_data_notification_keys: typing.Set[str]
# internals
# current message box through kvui
_messagebox = None
_messagebox: typing.Optional["kvui.MessageBox"] = None
# message box reporting a loss of connection
_messagebox_connection_loss: typing.Optional["kvui.MessageBox"] = None
def __init__(self, server_address, password):
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None:
# server state
self.server_address = server_address
self.username = None
@@ -171,7 +209,7 @@ class CommonContext:
self.hint_cost = None
self.slot_info = {}
self.permissions = {
"forfeit": "disabled",
"release": "disabled",
"collect": "disabled",
"remaining": "disabled",
}
@@ -192,6 +230,9 @@ class CommonContext:
self.server_locations = set() # all locations the server knows of, missing_location | checked_locations
self.locations_info = {}
self.stored_data = {}
self.stored_data_notification_keys = set()
self.input_queue = asyncio.Queue()
self.input_requests = 0
@@ -201,11 +242,17 @@ class CommonContext:
self.watcher_event = asyncio.Event()
self.jsontotextparser = JSONtoTextParser(self)
self.update_datapackage(network_data_package)
self.update_data_package(network_data_package)
# execution
self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy")
@property
def suggested_address(self) -> str:
if self.server_address:
return self.server_address
return Utils.persistent_load().get("client", {}).get("last_server_address", "")
@functools.cached_property
def raw_text_parser(self) -> RawJSONtoTextParser:
return RawJSONtoTextParser(self)
@@ -217,9 +264,9 @@ class CommonContext:
return len(self.checked_locations | self.missing_locations)
async def connection_closed(self):
self.reset_server_state()
if self.server and self.server.socket is not None:
await self.server.socket.close()
self.reset_server_state()
def reset_server_state(self):
self.auth = None
@@ -228,22 +275,28 @@ class CommonContext:
self.items_received = []
self.locations_info = {}
self.server_version = Version(0, 0, 0)
self.generator_version = Version(0, 0, 0)
self.server = None
self.server_task = None
self.hint_cost = None
self.permissions = {
"forfeit": "disabled",
"release": "disabled",
"collect": "disabled",
"remaining": "disabled",
}
async def disconnect(self):
async def disconnect(self, allow_autoreconnect: bool = False):
if not allow_autoreconnect:
self.disconnected_intentionally = True
if self.cancel_autoreconnect():
logger.info("Cancelled auto-reconnect.")
if self.server and not self.server.socket.closed:
await self.server.socket.close()
if self.server_task is not None:
await self.server_task
async def send_msgs(self, msgs):
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
""" `msgs` JSON serializable """
if not self.server or not self.server.socket.open or self.server.socket.closed:
return
await self.server.socket.send(encode(msgs))
@@ -271,25 +324,36 @@ class CommonContext:
logger.info('Enter slot name:')
self.auth = await self.console_input()
async def send_connect(self, **kwargs):
async def send_connect(self, **kwargs: typing.Any) -> None:
""" send `Connect` packet to log in to server """
payload = {
'cmd': 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
'tags': self.tags, 'items_handling': self.items_handling,
'uuid': Utils.get_unique_identifier(), 'game': self.game
'uuid': Utils.get_unique_identifier(), 'game': self.game, "slot_data": self.want_slot_data,
}
if kwargs:
payload.update(kwargs)
await self.send_msgs([payload])
async def console_input(self):
async def console_input(self) -> str:
if self.ui:
self.ui.focus_textinput()
self.input_requests += 1
return await self.input_queue.get()
async def connect(self, address=None):
async def connect(self, address: typing.Optional[str] = None) -> None:
""" disconnect any previous connection, and open new connection to the server """
await self.disconnect()
self.server_task = asyncio.create_task(server_loop(self, address), name="server loop")
def cancel_autoreconnect(self) -> bool:
if self.autoreconnect_task:
self.autoreconnect_task.cancel()
self.autoreconnect_task = None
return True
return False
def slot_concerns_self(self, slot) -> bool:
if slot == self.slot:
return True
@@ -297,6 +361,17 @@ class CommonContext:
return self.slot in self.slot_info[slot].group_members
return False
def is_echoed_chat(self, print_json_packet: dict) -> bool:
return print_json_packet.get("type", "") == "Chat" \
and print_json_packet.get("team", None) == self.team \
and print_json_packet.get("slot", None) == self.slot
def is_uninteresting_item_send(self, print_json_packet: dict) -> bool:
"""Helper function for filtering out ItemSend prints that do not concern the local player."""
return print_json_packet.get("type", "") == "ItemSend" \
and not self.slot_concerns_self(print_json_packet["receiving"]) \
and not self.slot_concerns_self(print_json_packet["item"].player)
def on_print(self, args: dict):
logger.info(args["text"])
@@ -328,6 +403,7 @@ class CommonContext:
async def shutdown(self):
self.server_address = ""
self.username = None
self.cancel_autoreconnect()
if self.server and not self.server.socket.closed:
await self.server.socket.close()
if self.server_task:
@@ -343,32 +419,40 @@ class CommonContext:
self.input_task.cancel()
# DataPackage
async def prepare_datapackage(self, relevant_games: typing.Set[str],
remote_datepackage_versions: typing.Dict[str, int]):
async def prepare_data_package(self, relevant_games: typing.Set[str],
remote_date_package_versions: typing.Dict[str, int],
remote_data_package_checksums: typing.Dict[str, str]):
"""Validate that all data is present for the current multiworld.
Download, assimilate and cache missing data from the server."""
# by documentation any game can use Archipelago locations/items -> always relevant
relevant_games.add("Archipelago")
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:
if game not in remote_date_package_versions and game not in remote_data_package_checksums:
continue
remote_version: int = remote_datepackage_versions[game]
if remote_version == 0: # custom datapackage for this game
remote_version: int = remote_date_package_versions.get(game, 0)
remote_checksum: typing.Optional[str] = remote_data_package_checksums.get(game)
if remote_version == 0 and not remote_checksum: # custom data package and no checksum for this game
needed_updates.add(game)
continue
local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
# no action required if local version is new enough
if remote_version > local_version:
cache_version: int = cache_package.get(game, {}).get("version", 0)
if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \
or remote_checksum != local_checksum:
cached_game = Utils.load_data_package_for_checksum(game, remote_checksum)
cache_version: int = cached_game.get("version", 0)
cache_checksum: typing.Optional[str] = cached_game.get("checksum")
# download remote version if cache is not new enough
if remote_version > cache_version:
if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
or remote_checksum != cache_checksum:
needed_updates.add(game)
else:
self.update_game(cache_package[game])
self.update_game(cached_game)
if needed_updates:
await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}])
@@ -378,19 +462,36 @@ class CommonContext:
for location_name, location_id in game_package["location_name_to_id"].items():
self.location_names[location_id] = location_name
def update_datapackage(self, data_package: dict):
for game, gamedata in data_package["games"].items():
self.update_game(gamedata)
def update_data_package(self, data_package: dict):
for game, game_data in data_package["games"].items():
self.update_game(game_data)
def consume_network_datapackage(self, data_package: dict):
self.update_datapackage(data_package)
def consume_network_data_package(self, data_package: dict):
self.update_data_package(data_package)
current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {})
current_cache.update(data_package["games"])
Utils.persistent_store("datapackage", "games", current_cache)
for game, game_data in data_package["games"].items():
Utils.store_data_package_for_checksum(game, game_data)
# data storage
def set_notify(self, *keys: str) -> None:
"""Subscribe to be notified of changes to selected data storage keys.
The values can be accessed via the "stored_data" attribute of this context, which is a dictionary mapping the
names of the data storage keys to the latest values received from the server.
"""
if new_keys := (set(keys) - self.stored_data_notification_keys):
self.stored_data_notification_keys.update(new_keys)
async_start(self.send_msgs([{"cmd": "Get",
"keys": list(new_keys)},
{"cmd": "SetNotify",
"keys": list(new_keys)}]))
# DeathLink hooks
def on_deathlink(self, data: dict):
def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
"""Gets dispatched when a new DeathLink is triggered by another linked player."""
self.last_death_link = max(data["time"], self.last_death_link)
text = data.get("cause", "")
@@ -421,10 +522,10 @@ class CommonContext:
if old_tags != self.tags and self.server and not self.server.socket.closed:
await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}])
def gui_error(self, title: str, text: typing.Union[Exception, str]):
def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Optional["kvui.MessageBox"]:
"""Displays an error messagebox"""
if not self.ui:
return
return None
title = title or "Error"
from kvui import MessageBox
if self._messagebox:
@@ -441,6 +542,13 @@ class CommonContext:
# display error
self._messagebox = MessageBox(title, text, error=True)
self._messagebox.open()
return self._messagebox
def handle_connection_loss(self, msg: str) -> None:
"""Helper for logging and displaying a loss of connection. Must be called from an except block."""
exc_info = sys.exc_info()
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
def run_gui(self):
"""Import kivy UI system and start running it as self.ui_task."""
@@ -477,7 +585,7 @@ async def keep_alive(ctx: CommonContext, seconds_between_checks=100):
seconds_elapsed = 0
async def server_loop(ctx: CommonContext, address=None):
async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None) -> None:
if ctx.server and ctx.server.socket:
logger.error('Already connected')
return
@@ -490,6 +598,11 @@ async def server_loop(ctx: CommonContext, address=None):
logger.info('Please connect to an Archipelago server.')
return
ctx.cancel_autoreconnect()
if ctx._messagebox_connection_loss:
ctx._messagebox_connection_loss.dismiss()
ctx._messagebox_connection_loss = None
address = f"ws://{address}" if "://" not in address \
else address.replace("archipelago://", "ws://")
@@ -500,40 +613,47 @@ async def server_loop(ctx: CommonContext, address=None):
ctx.password = server_url.password
port = server_url.port or 38281
def reconnect_hint() -> str:
return ", type /connect to reconnect" if ctx.server_address else ""
logger.info(f'Connecting to Archipelago server at {address}')
try:
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None,
ssl=get_ssl_context() if address.startswith("wss://") else None)
if ctx.ui is not None:
ctx.ui.update_address_bar(server_url.netloc)
ctx.server = Endpoint(socket)
logger.info('Connected')
ctx.server_address = address
ctx.current_reconnect_delay = ctx.starting_reconnect_delay
ctx.disconnected_intentionally = False
async for data in ctx.server.socket:
for msg in decode(data):
await process_server_cmd(ctx, msg)
logger.warning('Disconnected from multiworld server, type /connect to reconnect')
except ConnectionRefusedError as e:
msg = 'Connection refused by the server. May not be running Archipelago on that address or port.'
logger.exception(msg, extra={'compact_gui': True})
ctx.gui_error(msg, e)
except websockets.InvalidURI as e:
msg = 'Failed to connect to the multiworld server (invalid URI)'
logger.exception(msg, extra={'compact_gui': True})
ctx.gui_error(msg, e)
except OSError as e:
msg = 'Failed to connect to the multiworld server'
logger.exception(msg, extra={'compact_gui': True})
ctx.gui_error(msg, e)
except Exception as e:
msg = 'Lost connection to the multiworld server, type /connect to reconnect'
logger.exception(msg, extra={'compact_gui': True})
ctx.gui_error(msg, e)
logger.warning(f"Disconnected from multiworld server{reconnect_hint()}")
except websockets.InvalidMessage:
# probably encrypted
if address.startswith("ws://"):
# try wss
await server_loop(ctx, "ws" + address[1:])
else:
ctx.handle_connection_loss(f"Lost connection to the multiworld server due to InvalidMessage"
f"{reconnect_hint()}")
except ConnectionRefusedError:
ctx.handle_connection_loss("Connection refused by the server. "
"May not be running Archipelago on that address or port.")
except websockets.InvalidURI:
ctx.handle_connection_loss("Failed to connect to the multiworld server (invalid URI)")
except OSError:
ctx.handle_connection_loss("Failed to connect to the multiworld server")
except Exception:
ctx.handle_connection_loss(f"Lost connection to the multiworld server{reconnect_hint()}")
finally:
await ctx.connection_closed()
if ctx.server_address:
logger.info(f"... reconnecting in {ctx.current_reconnect_delay}s")
asyncio.create_task(server_autoreconnect(ctx), name="server auto reconnect")
if ctx.server_address and ctx.username and not ctx.disconnected_intentionally:
logger.info(f"... automatically reconnecting in {ctx.current_reconnect_delay} seconds")
assert ctx.autoreconnect_task is None
ctx.autoreconnect_task = asyncio.create_task(server_autoreconnect(ctx), name="server auto reconnect")
ctx.current_reconnect_delay *= 2
@@ -559,11 +679,16 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
logger.info('Room Information:')
logger.info('--------------------------------')
version = args["version"]
ctx.server_version = tuple(version)
version = ".".join(str(item) for item in version)
ctx.server_version = Version(*version)
logger.info(f'Server protocol version: {version}')
logger.info("Server protocol tags: " + ", ".join(args["tags"]))
if "generator_version" in args:
ctx.generator_version = Version(*args["generator_version"])
logger.info(f'Server protocol version: {ctx.server_version.as_simple_string()}, '
f'generator version: {ctx.generator_version.as_simple_string()}, '
f'tags: {", ".join(args["tags"])}')
else:
logger.info(f'Server protocol version: {ctx.server_version.as_simple_string()}, '
f'tags: {", ".join(args["tags"])}')
if args['password']:
logger.info('Password required')
ctx.update_permissions(args.get("permissions", {}))
@@ -588,14 +713,16 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
current_team = network_player.team
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
# update datapackage
await ctx.prepare_datapackage(set(args["games"]), args["datapackage_versions"])
# update data package
data_package_versions = args.get("datapackage_versions", {})
data_package_checksums = args.get("datapackage_checksums", {})
await ctx.prepare_data_package(set(args["games"]), data_package_versions, data_package_checksums)
await ctx.server_auth(args['password'])
elif cmd == 'DataPackage':
logger.info("Got new ID/Name DataPackage")
ctx.consume_network_datapackage(args['data'])
ctx.consume_network_data_package(args['data'])
elif cmd == 'ConnectionRefused':
errors = args["errors"]
@@ -623,6 +750,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
ctx.slot = args["slot"]
# int keys get lost in JSON transfer
ctx.slot_info = {int(pid): data for pid, data in args["slot_info"].items()}
ctx.hint_points = args.get("hint_points", 0)
ctx.consume_players_package(args["players"])
msgs = []
if ctx.locations_checked:
@@ -631,6 +759,11 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
if ctx.locations_scouted:
msgs.append({"cmd": "LocationScouts",
"locations": list(ctx.locations_scouted)})
if ctx.stored_data_notification_keys:
msgs.append({"cmd": "Get",
"keys": list(ctx.stored_data_notification_keys)})
msgs.append({"cmd": "SetNotify",
"keys": list(ctx.stored_data_notification_keys)})
if msgs:
await ctx.send_msgs(msgs)
if ctx.finished_game:
@@ -644,6 +777,9 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
ctx.checked_locations = set(args["checked_locations"])
ctx.server_locations = ctx.missing_locations | ctx. checked_locations
server_url = urllib.parse.urlparse(ctx.server_address)
Utils.persistent_store("client", "last_server_address", server_url.netloc)
elif cmd == 'ReceivedItems':
start_index = args["index"]
@@ -691,7 +827,12 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
# we can skip checking "DeathLink" in ctx.tags, as otherwise we wouldn't have been send this
if "DeathLink" in tags and ctx.last_death_link != args["data"]["time"]:
ctx.on_deathlink(args["data"])
elif cmd == "Retrieved":
ctx.stored_data.update(args["keys"])
elif cmd == "SetReply":
ctx.stored_data[args["key"]] = args["value"]
if args["key"] == "EnergyLink":
ctx.current_energy_link_value = args["value"]
if ctx.ui:
@@ -722,7 +863,7 @@ async def console_loop(ctx: CommonContext):
logger.exception(e)
def get_base_parser(description=None):
def get_base_parser(description: typing.Optional[str] = None):
import argparse
parser = argparse.ArgumentParser(description=description)
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
@@ -732,13 +873,13 @@ def get_base_parser(description=None):
return parser
if __name__ == '__main__':
# Text Mode to use !hint and such with games that have no text entry
def run_as_textclient():
class TextContext(CommonContext):
tags = {"AP", "IgnoreGame", "TextOnly"}
# Text Mode to use !hint and such with games that have no text entry
tags = {"AP", "TextOnly"}
game = "" # empty matches any game since 0.3.2
items_handling = 0b111 # receive all items for /received
want_slot_data = False # Can't use game specific slot_data
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
@@ -750,11 +891,13 @@ if __name__ == '__main__':
if cmd == "Connected":
self.game = self.slot_info[self.slot].game
async def disconnect(self, allow_autoreconnect: bool = False):
self.game = ""
await super().disconnect(allow_autoreconnect)
async def main(args):
ctx = TextContext(args.connect, args.password)
ctx.auth = args.name
ctx.server_address = args.connect
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
if gui_enabled:
@@ -764,7 +907,6 @@ if __name__ == '__main__':
await ctx.exit_event.wait()
await ctx.shutdown()
import colorama
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
@@ -784,3 +926,7 @@ if __name__ == '__main__':
asyncio.run(main(args))
colorama.deinit()
if __name__ == '__main__':
run_as_textclient()

View File

@@ -7,14 +7,15 @@ from typing import List
import Utils
from Utils import async_start
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
get_base_parser
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart ff1_connector.lua"
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure ff1_connector.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart ff1_connector.lua"
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart connector_ff1.lua"
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure connector_ff1.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart connector_ff1.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
@@ -32,7 +33,7 @@ class FF1CommandProcessor(ClientCommandProcessor):
logger.info(f"NES Status: {self.ctx.nes_status}")
def _cmd_toggle_msgs(self):
"""Toggle displaying messages in bizhawk"""
"""Toggle displaying messages in EmuHawk"""
global DISPLAY_MSGS
DISPLAY_MSGS = not DISPLAY_MSGS
logger.info(f"Messages are now {'enabled' if DISPLAY_MSGS else 'disabled'}")
@@ -69,7 +70,7 @@ class FF1Context(CommonContext):
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
asyncio.create_task(parse_locations(self.locations_array, self, True))
async_start(parse_locations(self.locations_array, self, True))
elif cmd == 'Print':
msg = args['text']
if ': !' not in msg:
@@ -180,7 +181,7 @@ async def nes_sync_task(ctx: FF1Context):
# print(data_decoded)
if ctx.game is not None and 'locations' in data_decoded:
# Not just a keep alive ping, parse
asyncio.create_task(parse_locations(data_decoded['locations'], ctx, False))
async_start(parse_locations(data_decoded['locations'], ctx, False))
if not ctx.auth:
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
if ctx.auth == '':

View File

@@ -1,433 +1,12 @@
from __future__ import annotations
import os
import logging
import json
import string
import copy
import subprocess
import time
import random
import ModuleUpdate
ModuleUpdate.update()
import factorio_rcon
import colorama
import asyncio
from queue import Queue
from worlds.factorio.Client import check_stdin, launch
import Utils
if __name__ == "__main__":
Utils.init_logging("FactorioClient", exception_logger="Client")
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, logger, gui_enabled, get_base_parser
from MultiServer import mark_raw
from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
from worlds.factorio import Factorio
class FactorioCommandProcessor(ClientCommandProcessor):
ctx: FactorioContext
@mark_raw
def _cmd_factorio(self, text: str) -> bool:
"""Send the following command to the bound Factorio Server."""
if self.ctx.rcon_client:
# TODO: Print the command non-silently only for race seeds, or otherwise block anything but /factorio /save in race seeds.
self.ctx.print_to_game(f"/factorio {text}")
result = self.ctx.rcon_client.send_command(text)
if result:
self.output(result)
return True
return False
def _cmd_resync(self):
"""Manually trigger a resync."""
self.ctx.awaiting_bridge = True
class FactorioContext(CommonContext):
command_processor = FactorioCommandProcessor
game = "Factorio"
items_handling = 0b111 # full remote
# updated by spinup server
mod_version: Utils.Version = Utils.Version(0, 0, 0)
def __init__(self, server_address, password):
super(FactorioContext, self).__init__(server_address, password)
self.send_index: int = 0
self.rcon_client = None
self.awaiting_bridge = False
self.write_data_path = None
self.death_link_tick: int = 0 # last send death link on Factorio layer
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
self.energy_link_increment = 0
self.last_deplete = 0
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(FactorioContext, self).server_auth(password_requested)
if self.rcon_client:
await get_info(self, self.rcon_client) # retrieve current auth code
else:
raise Exception("Cannot connect to a server with unknown own identity, "
"bridge to Factorio first.")
await self.send_connect()
def on_print(self, args: dict):
super(FactorioContext, self).on_print(args)
if self.rcon_client:
self.print_to_game(args['text'])
def on_print_json(self, args: dict):
if self.rcon_client:
text = self.factorio_json_text_parser(copy.deepcopy(args["data"]))
self.print_to_game(text)
super(FactorioContext, self).on_print_json(args)
@property
def savegame_name(self) -> str:
return f"AP_{self.seed_name}_{self.auth}_Save.zip"
def print_to_game(self, text):
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
f"{text}")
def on_deathlink(self, data: dict):
if self.rcon_client:
self.rcon_client.send_command(f"/ap-deathlink {data['source']}")
super(FactorioContext, self).on_deathlink(data)
def on_package(self, cmd: str, args: dict):
if cmd in {"Connected", "RoomUpdate"}:
# catch up sync anything that is already cleared.
if "checked_locations" in args and args["checked_locations"]:
self.rcon_client.send_commands({item_name: f'/ap-get-technology ap-{item_name}-\t-1' for
item_name in args["checked_locations"]})
if cmd == "Connected" and self.energy_link_increment:
asyncio.create_task(self.send_msgs([{
"cmd": "SetNotify", "keys": ["EnergyLink"]
}]))
elif cmd == "SetReply":
if args["key"] == "EnergyLink":
if self.energy_link_increment and args.get("last_deplete", -1) == self.last_deplete:
# it's our deplete request
gained = int(args["original_value"] - args["value"])
gained_text = Utils.format_SI_prefix(gained) + "J"
if gained:
logger.debug(f"EnergyLink: Received {gained_text}. "
f"{Utils.format_SI_prefix(args['value'])}J remaining.")
self.rcon_client.send_command(f"/ap-energylink {gained}")
def run_gui(self):
from kvui import GameManager
class FactorioManager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
("FactorioServer", "Factorio Server Log"),
("FactorioWatcher", "Bridge Data Log"),
]
base_title = "Archipelago Factorio Client"
self.ui = FactorioManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def game_watcher(ctx: FactorioContext):
bridge_logger = logging.getLogger("FactorioWatcher")
from worlds.factorio.Technologies import lookup_id_to_name
next_bridge = time.perf_counter() + 1
try:
while not ctx.exit_event.is_set():
# TODO: restore on-demand refresh
if ctx.rcon_client and time.perf_counter() > next_bridge:
next_bridge = time.perf_counter() + 1
ctx.awaiting_bridge = False
data = json.loads(ctx.rcon_client.send_command("/ap-sync"))
if not ctx.auth:
pass # auth failed, wait for new attempt
elif data["slot_name"] != ctx.auth:
bridge_logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}")
elif data["seed_name"] != ctx.seed_name:
bridge_logger.warning(
f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}")
else:
data = data["info"]
research_data = data["research_done"]
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
victory = data["victory"]
await ctx.update_death_link(data["death_link"])
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if ctx.locations_checked != research_data:
bridge_logger.debug(
f"New researches done: "
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
ctx.locations_checked = research_data
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
death_link_tick = data.get("death_link_tick", 0)
if death_link_tick != ctx.death_link_tick:
ctx.death_link_tick = death_link_tick
if "DeathLink" in ctx.tags:
asyncio.create_task(ctx.send_death())
if ctx.energy_link_increment:
in_world_bridges = data["energy_bridges"]
if in_world_bridges:
in_world_energy = data["energy"]
if in_world_energy < (ctx.energy_link_increment * in_world_bridges):
# attempt to refill
ctx.last_deplete = time.time()
asyncio.create_task(ctx.send_msgs([{
"cmd": "Set", "key": "EnergyLink", "operations":
[{"operation": "add", "value": -ctx.energy_link_increment * in_world_bridges},
{"operation": "max", "value": 0}],
"last_deplete": ctx.last_deplete
}]))
# Above Capacity - (len(Bridges) * ENERGY_INCREMENT)
elif in_world_energy > (in_world_bridges * ctx.energy_link_increment * 5) - \
ctx.energy_link_increment*in_world_bridges:
value = ctx.energy_link_increment * in_world_bridges
asyncio.create_task(ctx.send_msgs([{
"cmd": "Set", "key": "EnergyLink", "operations":
[{"operation": "add", "value": value}]
}]))
ctx.rcon_client.send_command(
f"/ap-energylink -{value}")
logger.debug(f"EnergyLink: Sent {Utils.format_SI_prefix(value)}J")
await asyncio.sleep(0.1)
except Exception as e:
logging.exception(e)
logging.error("Aborted Factorio Server Bridge")
def stream_factorio_output(pipe, queue, process):
def queuer():
while process.poll() is None:
text = pipe.readline().strip()
if text:
queue.put_nowait(text)
from threading import Thread
thread = Thread(target=queuer, name="Factorio Output Queue", daemon=True)
thread.start()
return thread
async def factorio_server_watcher(ctx: FactorioContext):
savegame_name = os.path.abspath(ctx.savegame_name)
if not os.path.exists(savegame_name):
logger.info(f"Creating savegame {savegame_name}")
subprocess.run((
executable, "--create", savegame_name, "--preset", "archipelago"
))
factorio_process = subprocess.Popen((executable, "--start-server", ctx.savegame_name,
*(str(elem) for elem in server_args)),
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL,
encoding="utf-8")
factorio_server_logger.info("Started Factorio Server")
factorio_queue = Queue()
stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process)
stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
try:
while not ctx.exit_event.is_set():
if factorio_process.poll():
factorio_server_logger.info("Factorio server has exited.")
ctx.exit_event.set()
while not factorio_queue.empty():
msg = factorio_queue.get()
factorio_queue.task_done()
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
if not ctx.server:
logger.info("Established bridge to Factorio Server. "
"Ready to connect to Archipelago via /connect")
if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg:
ctx.awaiting_bridge = True
factorio_server_logger.debug(msg)
else:
factorio_server_logger.info(msg)
if ctx.rcon_client:
commands = {}
while ctx.send_index < len(ctx.items_received):
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
item_id = transfer_item.item
player_name = ctx.player_names[transfer_item.player]
if item_id not in Factorio.item_id_to_name:
factorio_server_logger.error(f"Cannot send unknown item ID: {item_id}")
else:
item_name = Factorio.item_id_to_name[item_id]
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
commands[ctx.send_index] = f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}'
ctx.send_index += 1
if commands:
ctx.rcon_client.send_commands(commands)
await asyncio.sleep(0.1)
except Exception as e:
logging.exception(e)
logging.error("Aborted Factorio Server Bridge")
ctx.rcon_client = None
ctx.exit_event.set()
finally:
factorio_process.terminate()
factorio_process.wait(5)
async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient):
info = json.loads(rcon_client.send_command("/ap-rcon-info"))
ctx.auth = info["slot_name"]
ctx.seed_name = info["seed_name"]
# 0.2.0 addition, not present earlier
death_link = bool(info.get("death_link", False))
ctx.energy_link_increment = info.get("energy_link", 0)
logger.debug(f"Energy Link Increment: {ctx.energy_link_increment}")
if ctx.energy_link_increment and ctx.ui:
ctx.ui.enable_energy_link()
await ctx.update_death_link(death_link)
async def factorio_spinup_server(ctx: FactorioContext) -> bool:
savegame_name = os.path.abspath("Archipelago.zip")
if not os.path.exists(savegame_name):
logger.info(f"Creating savegame {savegame_name}")
subprocess.run((
executable, "--create", savegame_name
))
factorio_process = subprocess.Popen(
(executable, "--start-server", savegame_name, *(str(elem) for elem in server_args)),
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL,
encoding="utf-8")
factorio_server_logger.info("Started Information Exchange Factorio Server")
factorio_queue = Queue()
stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process)
stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
rcon_client = None
try:
while not ctx.auth:
while not factorio_queue.empty():
msg = factorio_queue.get()
factorio_server_logger.info(msg)
if "Loading mod AP-" in msg and msg.endswith("(data.lua)"):
parts = msg.split()
ctx.mod_version = Utils.Version(*(int(number) for number in parts[-2].split(".")))
elif "Write data path: " in msg:
ctx.write_data_path = Utils.get_text_between(msg, "Write data path: ", " [")
if "AppData" in ctx.write_data_path:
logger.warning("It appears your mods are loaded from Appdata, "
"this can lead to problems with multiple Factorio instances. "
"If this is the case, you will get a file locked error running Factorio.")
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
if ctx.mod_version == ctx.__class__.mod_version:
raise Exception("No Archipelago mod was loaded. Aborting.")
await get_info(ctx, rcon_client)
await asyncio.sleep(0.01)
except Exception as e:
logger.exception(e, extra={"compact_gui": True})
msg = "Aborted Factorio Server Bridge"
logger.error(msg)
ctx.gui_error(msg, e)
ctx.exit_event.set()
else:
logger.info(
f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
return True
finally:
factorio_process.terminate()
factorio_process.wait(5)
return False
async def main(args):
ctx = FactorioContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
factorio_server_task = asyncio.create_task(factorio_spinup_server(ctx), name="FactorioSpinupServer")
successful_launch = await factorio_server_task
if successful_launch:
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
progression_watcher = asyncio.create_task(
game_watcher(ctx), name="FactorioProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
await progression_watcher
await factorio_server_task
await ctx.shutdown()
class FactorioJSONtoTextParser(JSONtoTextParser):
def _handle_color(self, node: JSONMessagePart):
colors = node["color"].split(";")
for color in colors:
if color in self.color_codes:
node["text"] = f"[color=#{self.color_codes[color]}]{node['text']}[/color]"
return self._handle_text(node)
return self._handle_text(node)
if __name__ == '__main__':
parser = get_base_parser(description="Optional arguments to FactorioClient follow. "
"Remaining arguments get passed into bound Factorio instance."
"Refer to Factorio --help for those.")
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
parser.add_argument('--server-settings', help='Factorio server settings configuration file.')
args, rest = parser.parse_known_args()
colorama.init()
rcon_port = args.rcon_port
rcon_password = args.rcon_password if args.rcon_password else ''.join(
random.choice(string.ascii_letters) for x in range(32))
factorio_server_logger = logging.getLogger("FactorioServer")
options = Utils.get_options()
executable = options["factorio_options"]["executable"]
server_settings = args.server_settings if args.server_settings else options["factorio_options"].get("server_settings", None)
if server_settings:
server_settings = os.path.abspath(server_settings)
if not os.path.exists(os.path.dirname(executable)):
raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
if os.path.isdir(executable): # user entered a path to a directory, let's find the executable therein
executable = os.path.join(executable, "factorio")
if not os.path.isfile(executable):
if os.path.isfile(executable + ".exe"):
executable = executable + ".exe"
else:
raise FileNotFoundError(f"Path {executable} is not an executable file.")
if server_settings and os.path.isfile(server_settings):
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, "--server-settings", server_settings, *rest)
else:
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest)
asyncio.run(main(args))
colorama.deinit()
check_stdin()
launch()

406
Fill.py
View File

@@ -1,12 +1,12 @@
import logging
import typing
import collections
import itertools
import logging
import typing
from collections import Counter, deque
from BaseClasses import CollectionState, Location, LocationProgressType, MultiWorld, Item
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
from worlds.AutoWorld import call_all
from worlds.generic.Rules import add_item_rule
class FillError(RuntimeError):
@@ -22,13 +22,28 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
itempool: typing.List[Item], single_player_placement: bool = False, lock: bool = False) -> None:
item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
allow_partial: bool = False, allow_excluded: bool = False) -> None:
"""
:param world: Multiworld to be filled.
:param base_state: State assumed before fill.
:param locations: Locations to be filled with item_pool
:param item_pool: Items to fill into the locations
:param single_player_placement: if true, can speed up placement if everything belongs to a single player
:param lock: locations are set to locked as they are filled
:param swap: if true, swaps of already place items are done in the event of a dead end
:param on_place: callback that is called when a placement happens
:param allow_partial: only place what is possible. Remaining items will be in the item_pool list.
:param allow_excluded: if true and placement fails, it is re-attempted while ignoring excluded on Locations
"""
unplaced_items: typing.List[Item] = []
placements: typing.List[Location] = []
cleanup_required = False
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
swapped_items: typing.Counter[typing.Tuple[int, str, bool]] = Counter()
reachable_items: typing.Dict[int, typing.Deque[Item]] = {}
for item in itempool:
for item in item_pool:
reachable_items.setdefault(item.player, deque()).append(item)
while any(reachable_items.values()) and locations:
@@ -36,9 +51,9 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
items_to_place = [items.pop()
for items in reachable_items.values() if items]
for item in items_to_place:
itempool.remove(item)
item_pool.remove(item)
maximum_exploration_state = sweep_from_pool(
base_state, itempool + unplaced_items)
base_state, item_pool + unplaced_items)
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
@@ -69,62 +84,98 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
else:
# we filled all reachable spots.
# try swapping this item with previously placed items
for (i, location) in enumerate(placements):
placed_item = location.item
# Unplaceable items can sometimes be swapped infinitely. Limit the
# number of times we will swap an individual item to prevent this
swap_count = swapped_items[placed_item.player,
placed_item.name]
if swap_count > 1:
if swap:
# try swapping this item with previously placed items in a safe way then in an unsafe way
swap_attempts = ((i, location, unsafe)
for unsafe in (False, True)
for i, location in enumerate(placements))
for (i, location, unsafe) in swap_attempts:
placed_item = location.item
# Unplaceable items can sometimes be swapped infinitely. Limit the
# number of times we will swap an individual item to prevent this
swap_count = swapped_items[placed_item.player, placed_item.name, unsafe]
if swap_count > 1:
continue
location.item = None
placed_item.location = None
swap_state = sweep_from_pool(base_state, [placed_item] if unsafe else [])
# unsafe means swap_state assumes we can somehow collect placed_item before item_to_place
# by continuing to swap, which is not guaranteed. This is unsafe because there is no mechanic
# to clean that up later, so there is a chance generation fails.
if (not single_player_placement or location.player == item_to_place.player) \
and location.can_fill(swap_state, item_to_place, perform_access_check):
# Verify placing this item won't reduce available locations, which would be a useless swap.
prev_state = swap_state.copy()
prev_loc_count = len(
world.get_reachable_locations(prev_state))
swap_state.collect(item_to_place, True)
new_loc_count = len(
world.get_reachable_locations(swap_state))
if new_loc_count >= prev_loc_count:
# Add this item to the existing placement, and
# add the old item to the back of the queue
spot_to_fill = placements.pop(i)
swap_count += 1
swapped_items[placed_item.player, placed_item.name, unsafe] = swap_count
reachable_items[placed_item.player].appendleft(
placed_item)
item_pool.append(placed_item)
# cleanup at the end to hopefully get better errors
cleanup_required = True
break
# Item can't be placed here, restore original item
location.item = placed_item
placed_item.location = location
if spot_to_fill is None:
# Can't place this item, move on to the next
unplaced_items.append(item_to_place)
continue
location.item = None
placed_item.location = None
swap_state = sweep_from_pool(base_state)
if (not single_player_placement or location.player == item_to_place.player) \
and location.can_fill(swap_state, item_to_place, perform_access_check):
# Verify that placing this item won't reduce available locations
prev_state = swap_state.copy()
prev_state.collect(placed_item)
prev_loc_count = len(
world.get_reachable_locations(prev_state))
swap_state.collect(item_to_place, True)
new_loc_count = len(
world.get_reachable_locations(swap_state))
if new_loc_count >= prev_loc_count:
# Add this item to the existing placement, and
# add the old item to the back of the queue
spot_to_fill = placements.pop(i)
swap_count += 1
swapped_items[placed_item.player,
placed_item.name] = swap_count
reachable_items[placed_item.player].appendleft(
placed_item)
itempool.append(placed_item)
break
# Item can't be placed here, restore original item
location.item = placed_item
placed_item.location = location
if spot_to_fill is None:
# Can't place this item, move on to the next
else:
unplaced_items.append(item_to_place)
continue
world.push_item(spot_to_fill, item_to_place, False)
spot_to_fill.locked = lock
placements.append(spot_to_fill)
spot_to_fill.event = item_to_place.advancement
if on_place:
on_place(spot_to_fill)
if len(unplaced_items) > 0 and len(locations) > 0:
if cleanup_required:
# validate all placements and remove invalid ones
for placement in placements:
state = sweep_from_pool(base_state, [])
if world.accessibility[placement.item.player] != "minimal" and not placement.can_reach(state):
placement.item.location = None
unplaced_items.append(placement.item)
placement.item = None
locations.append(placement)
if allow_excluded:
# check if partial fill is the result of excluded locations, in which case retry
excluded_locations = [
location for location in locations
if location.progress_type == location.progress_type.EXCLUDED and not location.item
]
if excluded_locations:
for location in excluded_locations:
location.progress_type = location.progress_type.DEFAULT
fill_restrictive(world, base_state, excluded_locations, unplaced_items, single_player_placement, lock,
swap, on_place, allow_partial, False)
for location in excluded_locations:
if not location.item:
location.progress_type = location.progress_type.EXCLUDED
if not allow_partial and len(unplaced_items) > 0 and len(locations) > 0:
# There are leftover unplaceable items and locations that won't accept them
if world.can_beat_game():
logging.warning(
@@ -133,7 +184,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
itempool.extend(unplaced_items)
item_pool.extend(unplaced_items)
def remaining_fill(world: MultiWorld,
@@ -209,12 +260,130 @@ def fast_fill(world: MultiWorld,
return item_pool[placing:], fill_locations[placing:]
def accessibility_corrections(world: MultiWorld, state: CollectionState, locations, pool=[]):
maximum_exploration_state = sweep_from_pool(state, pool)
minimal_players = {player for player in world.player_ids if world.accessibility[player] == "minimal"}
unreachable_locations = [location for location in world.get_locations() if location.player in minimal_players and
not location.can_reach(maximum_exploration_state)]
for location in unreachable_locations:
if (location.item is not None and location.item.advancement and location.address is not None and not
location.locked and location.item.player not in minimal_players):
pool.append(location.item)
state.remove(location.item)
location.item = None
location.event = False
if location in state.events:
state.events.remove(location)
locations.append(location)
if pool and locations:
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
fill_restrictive(world, state, locations, pool)
def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations):
maximum_exploration_state = sweep_from_pool(state)
unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)]
if unreachable_locations:
def forbid_important_item_rule(item: Item):
return not ((item.classification & 0b0011) and world.accessibility[item.player] != 'minimal')
for location in unreachable_locations:
add_item_rule(location, forbid_important_item_rule)
def distribute_early_items(world: MultiWorld,
fill_locations: typing.List[Location],
itempool: typing.List[Item]) -> typing.Tuple[typing.List[Location], typing.List[Item]]:
""" returns new fill_locations and itempool """
early_items_count: typing.Dict[typing.Tuple[str, int], typing.List[int]] = {}
for player in world.player_ids:
items = itertools.chain(world.early_items[player], world.local_early_items[player])
for item in items:
early_items_count[item, player] = [world.early_items[player].get(item, 0),
world.local_early_items[player].get(item, 0)]
if early_items_count:
early_locations: typing.List[Location] = []
early_priority_locations: typing.List[Location] = []
loc_indexes_to_remove: typing.Set[int] = set()
base_state = world.state.copy()
base_state.sweep_for_events(locations=(loc for loc in world.get_filled_locations() if loc.address is None))
for i, loc in enumerate(fill_locations):
if loc.can_reach(base_state):
if loc.progress_type == LocationProgressType.PRIORITY:
early_priority_locations.append(loc)
else:
early_locations.append(loc)
loc_indexes_to_remove.add(i)
fill_locations = [loc for i, loc in enumerate(fill_locations) if i not in loc_indexes_to_remove]
early_prog_items: typing.List[Item] = []
early_rest_items: typing.List[Item] = []
early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids}
early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids}
item_indexes_to_remove: typing.Set[int] = set()
for i, item in enumerate(itempool):
if (item.name, item.player) in early_items_count:
if item.advancement:
if early_items_count[item.name, item.player][1]:
early_local_prog_items[item.player].append(item)
early_items_count[item.name, item.player][1] -= 1
else:
early_prog_items.append(item)
early_items_count[item.name, item.player][0] -= 1
else:
if early_items_count[item.name, item.player][1]:
early_local_rest_items[item.player].append(item)
early_items_count[item.name, item.player][1] -= 1
else:
early_rest_items.append(item)
early_items_count[item.name, item.player][0] -= 1
item_indexes_to_remove.add(i)
if early_items_count[item.name, item.player] == [0, 0]:
del early_items_count[item.name, item.player]
if len(early_items_count) == 0:
break
itempool = [item for i, item in enumerate(itempool) if i not in item_indexes_to_remove]
for player in world.player_ids:
player_local = early_local_rest_items[player]
fill_restrictive(world, base_state,
[loc for loc in early_locations if loc.player == player],
player_local, lock=True, allow_partial=True)
if player_local:
logging.warning(f"Could not fulfill rules of early items: {player_local}")
early_rest_items.extend(early_local_rest_items[player])
early_locations = [loc for loc in early_locations if not loc.item]
fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True)
early_locations += early_priority_locations
for player in world.player_ids:
player_local = early_local_prog_items[player]
fill_restrictive(world, base_state,
[loc for loc in early_locations if loc.player == player],
player_local, lock=True, allow_partial=True)
if player_local:
logging.warning(f"Could not fulfill rules of early items: {player_local}")
early_prog_items.extend(player_local)
early_locations = [loc for loc in early_locations if not loc.item]
fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True)
unplaced_early_items = early_rest_items + early_prog_items
if unplaced_early_items:
logging.warning("Ran out of early locations for early items. Failed to place "
f"{unplaced_early_items} early.")
itempool += unplaced_early_items
fill_locations.extend(early_locations)
world.random.shuffle(fill_locations)
return fill_locations, itempool
def distribute_items_restrictive(world: MultiWorld) -> None:
fill_locations = sorted(world.get_unfilled_locations())
world.random.shuffle(fill_locations)
# get items to distribute
itempool = sorted(world.itempool)
world.random.shuffle(itempool)
fill_locations, itempool = distribute_early_items(world, fill_locations, itempool)
progitempool: typing.List[Item] = []
usefulitempool: typing.List[Item] = []
filleritempool: typing.List[Item] = []
@@ -239,15 +408,33 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
defaultlocations = locations[LocationProgressType.DEFAULT]
excludedlocations = locations[LocationProgressType.EXCLUDED]
fill_restrictive(world, world.state, prioritylocations, progitempool, lock=True)
# can't lock due to accessibility corrections touching things, so we remember which ones got placed and lock later
lock_later = []
def mark_for_locking(location: Location):
nonlocal lock_later
lock_later.append(location)
if prioritylocations:
# "priority fill"
fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking)
accessibility_corrections(world, world.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations
if progitempool:
# "progression fill"
fill_restrictive(world, world.state, defaultlocations, progitempool)
if progitempool:
raise FillError(
f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
accessibility_corrections(world, world.state, defaultlocations)
for location in lock_later:
if location.item:
location.locked = True
del mark_for_locking, lock_later
inaccessible_location_rules(world, world.state, defaultlocations)
remaining_fill(world, excludedlocations, filleritempool)
if excludedlocations:
@@ -354,16 +541,16 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
checked_locations: typing.Set[Location] = set()
unchecked_locations: typing.Set[Location] = set(world.get_locations())
reachable_locations_count: typing.Dict[int, int] = {
player: 0
for player in world.player_ids
if len(world.get_filled_locations(player)) != 0
}
total_locations_count: typing.Counter[int] = Counter(
location.player
for location in world.get_locations()
if not location.locked
)
reachable_locations_count: typing.Dict[int, int] = {
player: 0
for player in world.player_ids
if total_locations_count[player] and len(world.get_filled_locations(player)) != 0
}
balanceable_players = {
player: balanceable_players[player]
for player in balanceable_players
@@ -380,6 +567,10 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
def item_percentage(player: int, num: int) -> float:
return num / total_locations_count[player]
# If there are no locations that aren't locked, there's no point in attempting to balance progression.
if len(total_locations_count) == 0:
return
while True:
# Gather non-locked locations.
# This ensures that only shuffled locations get counted for progression balancing,
@@ -548,6 +739,17 @@ def distribute_planned(world: MultiWorld) -> None:
else:
warn(warning, force)
swept_state = world.state.copy()
swept_state.sweep_for_events()
reachable = frozenset(world.get_reachable_locations(swept_state))
early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
for loc in world.get_unfilled_locations():
if loc in reachable:
early_locations[loc.player].append(loc.name)
else: # not reachable with swept state
non_early_locations[loc.player].append(loc.name)
# TODO: remove. Preferably by implementing key drop
from worlds.alttp.Regions import key_drop_data
world_name_lookup = world.world_name_lookup
@@ -563,7 +765,39 @@ def distribute_planned(world: MultiWorld) -> None:
if 'from_pool' not in block:
block['from_pool'] = True
if 'world' not in block:
block['world'] = False
target_world = False
else:
target_world = block['world']
if target_world is False or world.players == 1: # target own world
worlds: typing.Set[int] = {player}
elif target_world is True: # target any worlds besides own
worlds = set(world.player_ids) - {player}
elif target_world is None: # target all worlds
worlds = set(world.player_ids)
elif type(target_world) == list: # list of target worlds
worlds = set()
for listed_world in target_world:
if listed_world not in world_name_lookup:
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
block['force'])
continue
worlds.add(world_name_lookup[listed_world])
elif type(target_world) == int: # target world by slot number
if target_world not in range(1, world.players + 1):
failed(
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
block['force'])
continue
worlds = {target_world}
else: # target world by slot name
if target_world not in world_name_lookup:
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
block['force'])
continue
worlds = {world_name_lookup[target_world]}
block['world'] = worlds
items: block_value = []
if "items" in block:
items = block["items"]
@@ -600,6 +834,16 @@ def distribute_planned(world: MultiWorld) -> None:
for key, value in locations.items():
location_list += [key] * value
locations = location_list
if "early_locations" in locations:
locations.remove("early_locations")
for player in worlds:
locations += early_locations[player]
if "non_early_locations" in locations:
locations.remove("non_early_locations")
for player in worlds:
locations += non_early_locations[player]
block['locations'] = locations
if not block['count']:
@@ -635,41 +879,13 @@ def distribute_planned(world: MultiWorld) -> None:
for placement in plando_blocks:
player = placement['player']
try:
target_world = placement['world']
worlds = placement['world']
locations = placement['locations']
items = placement['items']
maxcount = placement['count']['target']
from_pool = placement['from_pool']
if target_world is False or world.players == 1: # target own world
worlds: typing.Set[int] = {player}
elif target_world is True: # target any worlds besides own
worlds = set(world.player_ids) - {player}
elif target_world is None: # target all worlds
worlds = set(world.player_ids)
elif type(target_world) == list: # list of target worlds
worlds = set()
for listed_world in target_world:
if listed_world not in world_name_lookup:
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
placement['force'])
continue
worlds.add(world_name_lookup[listed_world])
elif type(target_world) == int: # target world by slot number
if target_world not in range(1, world.players + 1):
failed(
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
placement['force'])
continue
worlds = {target_world}
else: # target world by slot name
if target_world not in world_name_lookup:
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
placement['force'])
continue
worlds = {world_name_lookup[target_world]}
candidates = list(location for location in world.get_unfilled_locations_for_players(locations,
worlds))
candidates = list(world.get_unfilled_locations_for_players(locations, sorted(worlds)))
world.random.shuffle(candidates)
world.random.shuffle(items)
count = 0

View File

@@ -2,14 +2,13 @@ from __future__ import annotations
import argparse
import logging
import random
import urllib.request
import urllib.parse
from typing import Set, Dict, Tuple, Callable, Any, Union
import os
from collections import Counter, ChainMap
import random
import string
import enum
import urllib.parse
import urllib.request
from collections import ChainMap, Counter
from typing import Any, Callable, Dict, Tuple, Union
import ModuleUpdate
@@ -18,54 +17,16 @@ ModuleUpdate.update()
import Utils
from worlds.alttp import Options as LttPOptions
from worlds.generic import PlandoConnection
from Utils import parse_yamls, version_tuple, __version__, tuplize_version, get_options, local_path, user_path
from Utils import parse_yamls, version_tuple, __version__, tuplize_version, get_options, user_path
from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain
from BaseClasses import seeddigits, get_seed
from BaseClasses import seeddigits, get_seed, PlandoOptions
import Options
from worlds.alttp.Text import TextTable
from worlds.AutoWorld import AutoWorldRegister
import copy
class PlandoSettings(enum.IntFlag):
items = 0b0001
connections = 0b0010
texts = 0b0100
bosses = 0b1000
@classmethod
def from_option_string(cls, option_string: str) -> PlandoSettings:
result = cls(0)
for part in option_string.split(","):
part = part.strip().lower()
if part:
result = cls._handle_part(part, result)
return result
@classmethod
def from_set(cls, option_set: Set[str]) -> PlandoSettings:
result = cls(0)
for part in option_set:
result = cls._handle_part(part, result)
return result
@classmethod
def _handle_part(cls, part: str, base: PlandoSettings) -> PlandoSettings:
try:
part = cls[part]
except Exception as e:
raise KeyError(f"{part} is not a recognized name for a plando module. "
f"Known options: {', '.join(flag.name for flag in cls)}") from e
else:
return base | part
def __str__(self) -> str:
if self.value:
return ", ".join(flag.name for flag in PlandoSettings if self.value & flag.value)
return "Off"
def mystery_argparse():
options = get_options()
defaults = options["generator"]
@@ -92,12 +53,14 @@ def mystery_argparse():
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
parser.add_argument('--plando', default=defaults["plando_options"],
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
parser.add_argument("--skip_prog_balancing", action="store_true",
help="Skip progression balancing step during generation.")
args = parser.parse_args()
if not os.path.isabs(args.weights_file_path):
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
if not os.path.isabs(args.meta_file_path):
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
args.plando: PlandoSettings = PlandoSettings.from_option_string(args.plando)
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
return args, options
@@ -110,10 +73,12 @@ def main(args=None, callback=ERmain):
args, options = mystery_argparse()
seed = get_seed(args.seed)
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
random.seed(seed)
seed_name = get_seed_name(random)
if args.race:
logging.info("Race mode enabled. Using non-deterministic random source.")
random.seed() # reset to time-based random source
weights_cache: Dict[str, Tuple[Any, ...]] = {}
@@ -122,15 +87,15 @@ def main(args=None, callback=ERmain):
weights_cache[args.weights_file_path] = read_weights_yamls(args.weights_file_path)
except Exception as e:
raise ValueError(f"File {args.weights_file_path} is destroyed. Please fix your yaml.") from e
print(f"Weights: {args.weights_file_path} >> "
f"{get_choice('description', weights_cache[args.weights_file_path][-1], 'No description specified')}")
logging.info(f"Weights: {args.weights_file_path} >> "
f"{get_choice('description', weights_cache[args.weights_file_path][-1], 'No description specified')}")
if args.meta_file_path and os.path.exists(args.meta_file_path):
try:
meta_weights = read_weights_yamls(args.meta_file_path)[-1]
except Exception as e:
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
logging.info(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file
del(meta_weights["meta_description"])
except Exception as e:
@@ -143,7 +108,7 @@ def main(args=None, callback=ERmain):
player_files = {}
for file in os.scandir(args.player_files_path):
fname = file.name
if file.is_file() and not file.name.startswith(".") and \
if file.is_file() and not fname.startswith(".") and \
os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}:
path = os.path.join(args.player_files_path, fname)
try:
@@ -154,28 +119,30 @@ def main(args=None, callback=ERmain):
# sort dict for consistent results across platforms:
weights_cache = {key: value for key, value in sorted(weights_cache.items())}
for filename, yaml_data in weights_cache.items():
for yaml in yaml_data:
print(f"P{player_id} Weights: {filename} >> "
f"{get_choice('description', yaml, 'No description specified')}")
player_files[player_id] = filename
player_id += 1
if filename not in {args.meta_file_path, args.weights_file_path}:
for yaml in yaml_data:
logging.info(f"P{player_id} Weights: {filename} >> "
f"{get_choice('description', yaml, 'No description specified')}")
player_files[player_id] = filename
player_id += 1
args.multi = max(player_id - 1, args.multi)
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: "
f"{args.plando}")
logging.info(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, "
f"{seed_name} Seed {seed} with plando: {args.plando}")
if not weights_cache:
raise Exception(f"No weights found. Provide a general weights file ({args.weights_file_path}) or individual player files. "
raise Exception(f"No weights found. "
f"Provide a general weights file ({args.weights_file_path}) or individual player files. "
f"A mix is also permitted.")
erargs = parse_arguments(['--multi', str(args.multi)])
erargs.seed = seed
erargs.plando_options = args.plando
erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
erargs.spoiler = args.spoiler
erargs.race = args.race
erargs.outputname = seed_name
erargs.outputpath = args.outputpath
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
erargs.skip_prog_balancing = args.skip_prog_balancing
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
@@ -225,15 +192,15 @@ def main(args=None, callback=ERmain):
elif not erargs.name[player]: # if name was not specified, generate it from filename
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
player += 1
except Exception as e:
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
else:
raise RuntimeError(f'No weights specified for player {player}')
if len(set(erargs.name.values())) != len(erargs.name):
raise Exception(f"Names have to be unique. Names: {Counter(erargs.name.values())}")
if len(set(name.lower() for name in erargs.name.values())) != len(erargs.name):
raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in erargs.name.values())}")
if args.yaml_output:
import yaml
@@ -316,11 +283,11 @@ class SafeDict(dict):
def handle_name(name: str, player: int, name_counter: Counter):
name_counter[name] += 1
name_counter[name.lower()] += 1
number = name_counter[name.lower()]
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=name_counter[name],
NUMBER=(name_counter[name] if name_counter[
name] > 1 else ''),
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=number,
NUMBER=(number if number > 1 else ''),
player=player,
PLAYER=(player if player > 1 else '')))
new_name = new_name.strip()[:16]
@@ -377,7 +344,7 @@ def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
if option_key in options:
if options[option_key].supports_weighting:
return get_choice(option_key, category_dict)
return options[option_key]
return category_dict[option_key]
if game == "A Link to the Past": # TODO wow i hate this
if option_key in {"glitches_required", "dark_room_logic", "entrance_shuffle", "goals", "triforce_pieces_mode",
"triforce_pieces_percentage", "triforce_pieces_available", "triforce_pieces_extra",
@@ -442,7 +409,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
return weights
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoSettings):
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoOptions):
if option_key in game_weights:
try:
if not option.supports_weighting:
@@ -458,7 +425,7 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
setattr(ret, option_key, option.from_any(option.default)) # call the from_any here to support default "random"
def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings.bosses):
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
if "linked_options" in weights:
weights = roll_linked_options(weights)
@@ -471,7 +438,7 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings
if tuplize_version(version) > version_tuple:
raise Exception(f"Settings reports required version of generator is at least {version}, "
f"however generator is of version {__version__}")
required_plando_options = PlandoSettings.from_option_string(requirements.get("plando", ""))
required_plando_options = PlandoOptions.from_option_string(requirements.get("plando", ""))
if required_plando_options not in plando_options:
if required_plando_options:
raise Exception(f"Settings reports required plando module {str(required_plando_options)}, "
@@ -483,6 +450,11 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
ret.game = get_choice("game", weights)
if ret.game not in AutoWorldRegister.world_types:
picks = Utils.get_fuzzy_results(ret.game, AutoWorldRegister.world_types, limit=1)[0]
raise Exception(f"No world found to handle game {ret.game}. Did you mean '{picks[0]}' ({picks[1]}% sure)? "
f"Check your spelling or installation of that world.")
if ret.game not in weights:
raise Exception(f"No game options for selected game \"{ret.game}\" found.")
@@ -497,31 +469,29 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings
for option_key, option in Options.common_options.items():
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
if ret.game in AutoWorldRegister.world_types:
for option_key, option in world_type.option_definitions.items():
for option_key, option in world_type.option_definitions.items():
handle_option(ret, game_weights, option_key, option, plando_options)
for option_key, option in Options.per_game_common_options.items():
# skip setting this option if already set from common_options, defaulting to root option
if option_key not in world_type.option_definitions and \
(option_key not in Options.common_options or option_key in game_weights):
handle_option(ret, game_weights, option_key, option, plando_options)
for option_key, option in Options.per_game_common_options.items():
# skip setting this option if already set from common_options, defaulting to root option
if not (option_key in Options.common_options and option_key not in game_weights):
handle_option(ret, game_weights, option_key, option, plando_options)
if PlandoSettings.items in plando_options:
ret.plando_items = game_weights.get("plando_items", [])
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
# bad hardcoded behavior to make this work for now
ret.plando_connections = []
if PlandoSettings.connections in plando_options:
options = game_weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement),
get_choice("exit", placement),
get_choice("direction", placement)
))
elif ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights, plando_options)
else:
raise Exception(f"Unsupported game {ret.game}")
if PlandoOptions.items in plando_options:
ret.plando_items = game_weights.get("plando_items", [])
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
# bad hardcoded behavior to make this work for now
ret.plando_connections = []
if PlandoOptions.connections in plando_options:
options = game_weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement),
get_choice("exit", placement),
get_choice("direction", placement)
))
elif ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights, plando_options)
return ret
@@ -624,7 +594,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
ret.plando_texts = {}
if PlandoSettings.texts in plando_options:
if PlandoOptions.texts in plando_options:
tt = TextTable()
tt.removeUnwantedText()
options = weights.get("plando_texts", [])
@@ -636,7 +606,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.plando_texts[at] = str(get_choice_legacy("text", placement))
ret.plando_connections = []
if PlandoSettings.connections in plando_options:
if PlandoOptions.connections in plando_options:
options = weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice_legacy("percentage", placement, 100)):

894
KH2Client.py Normal file
View File

@@ -0,0 +1,894 @@
import os
import asyncio
import ModuleUpdate
import json
import Utils
from pymem import pymem
from worlds.kh2.Items import exclusionItem_table, CheckDupingItems
from worlds.kh2 import all_locations, item_dictionary_table, exclusion_table
from worlds.kh2.WorldLocations import *
from worlds import network_data_package
if __name__ == "__main__":
Utils.init_logging("KH2Client", exception_logger="Client")
from NetUtils import ClientStatus
from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \
CommonContext, server_loop
ModuleUpdate.update()
kh2_loc_name_to_id = network_data_package["games"]["Kingdom Hearts 2"]["location_name_to_id"]
# class KH2CommandProcessor(ClientCommandProcessor):
class KH2Context(CommonContext):
# command_processor: int = KH2CommandProcessor
game = "Kingdom Hearts 2"
items_handling = 0b101 # Indicates you get items sent from other worlds.
def __init__(self, server_address, password):
super(KH2Context, self).__init__(server_address, password)
self.kh2LocalItems = None
self.ability = None
self.growthlevel = None
self.KH2_sync_task = None
self.syncing = False
self.kh2connected = False
self.serverconneced = False
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
self.location_name_to_data = {name: data for name, data, in all_locations.items()}
self.lookup_id_to_item: typing.Dict[int, str] = {data.code: item_name for item_name, data in
item_dictionary_table.items() if data.code}
self.lookup_id_to_Location: typing.Dict[int, str] = {data.code: item_name for item_name, data in
all_locations.items() if data.code}
self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()}
self.location_table = {}
self.collectible_table = {}
self.collectible_override_flags_address = 0
self.collectible_offsets = {}
self.sending = []
# list used to keep track of locations+items player has. Used for disoneccting
self.kh2seedsave = None
self.slotDataProgressionNames = {}
self.kh2seedname = None
self.kh2slotdata = None
self.itemamount = {}
# sora equipped, valor equipped, master equipped, final equipped
self.keybladeAnchorList = (0x24F0, 0x32F4, 0x339C, 0x33D4)
if "localappdata" in os.environ:
self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP")
self.amountOfPieces = 0
# hooked object
self.kh2 = None
self.ItemIsSafe = False
self.game_connected = False
self.finalxemnas = False
self.worldid = {
# 1: {}, # world of darkness (story cutscenes)
2: TT_Checks,
# 3: {}, # destiny island doesn't have checks to ima put tt checks here
4: HB_Checks,
5: BC_Checks,
6: Oc_Checks,
7: AG_Checks,
8: LoD_Checks,
9: HundredAcreChecks,
10: PL_Checks,
11: DC_Checks, # atlantica isn't a supported world. if you go in atlantica it will check dc
12: DC_Checks,
13: TR_Checks,
14: HT_Checks,
15: HB_Checks, # world map, but you only go to the world map while on the way to goa so checking hb
16: PR_Checks,
17: SP_Checks,
18: TWTNW_Checks,
# 255: {}, # starting screen
}
# 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room
self.sveroom = 0x2A09C00 + 0x41
# 0 not in battle 1 in yellow battle 2 red battle #short
self.inBattle = 0x2A0EAC4 + 0x40
self.onDeath = 0xAB9078
# PC Address anchors
self.Now = 0x0714DB8
self.Save = 0x09A70B0
self.Sys3 = 0x2A59DF0
self.Bt10 = 0x2A74880
self.BtlEnd = 0x2A0D3E0
self.Slot1 = 0x2A20C98
self.chest_set = set(exclusion_table["Chests"])
self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"])
self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"])
self.shield_set = set(CheckDupingItems["Weapons"]["Shields"])
self.all_weapons = self.keyblade_set.union(self.staff_set).union(self.shield_set)
self.equipment_categories = CheckDupingItems["Equipment"]
self.armor_set = set(self.equipment_categories["Armor"])
self.accessories_set = set(self.equipment_categories["Accessories"])
self.all_equipment = self.armor_set.union(self.accessories_set)
self.Equipment_Anchor_Dict = {
"Armor": [0x2504, 0x2506, 0x2508, 0x250A],
"Accessories": [0x2514, 0x2516, 0x2518, 0x251A]}
self.AbilityQuantityDict = {}
self.ability_categories = CheckDupingItems["Abilities"]
self.sora_ability_set = set(self.ability_categories["Sora"])
self.donald_ability_set = set(self.ability_categories["Donald"])
self.goofy_ability_set = set(self.ability_categories["Goofy"])
self.all_abilities = self.sora_ability_set.union(self.donald_ability_set).union(self.goofy_ability_set)
self.boost_set = set(CheckDupingItems["Boosts"])
self.stat_increase_set = set(CheckDupingItems["Stat Increases"])
self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities}
# Growth:[level 1,level 4,slot]
self.growth_values_dict = {"High Jump": [0x05E, 0x061, 0x25DA],
"Quick Run": [0x62, 0x65, 0x25DC],
"Dodge Roll": [0x234, 0x237, 0x25DE],
"Aerial Dodge": [0x066, 0x069, 0x25E0],
"Glide": [0x6A, 0x6D, 0x25E2]}
self.boost_to_anchor_dict = {
"Power Boost": 0x24F9,
"Magic Boost": 0x24FA,
"Defense Boost": 0x24FB,
"AP Boost": 0x24F8}
self.AbilityCodeList = [self.item_name_to_data[item].code for item in exclusionItem_table["Ability"]]
self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}
self.bitmask_item_code = [
0x130000, 0x130001, 0x130002, 0x130003, 0x130004, 0x130005, 0x130006, 0x130007
, 0x130008, 0x130009, 0x13000A, 0x13000B, 0x13000C
, 0x13001F, 0x130020, 0x130021, 0x130022, 0x130023
, 0x13002A, 0x13002B, 0x13002C, 0x13002D]
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(KH2Context, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
async def connection_closed(self):
self.kh2connected = False
self.serverconneced = False
if self.kh2seedname is not None and self.auth is not None:
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
'w') as f:
f.write(json.dumps(self.kh2seedsave, indent=4))
await super(KH2Context, self).connection_closed()
async def disconnect(self, allow_autoreconnect: bool = False):
self.kh2connected = False
self.serverconneced = False
if self.kh2seedname not in {None} and self.auth not in {None}:
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
'w') as f:
f.write(json.dumps(self.kh2seedsave, indent=4))
await super(KH2Context, self).disconnect()
@property
def endpoints(self):
if self.server:
return [self.server]
else:
return []
async def shutdown(self):
if self.kh2seedname not in {None} and self.auth not in {None}:
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
'w') as f:
f.write(json.dumps(self.kh2seedsave, indent=4))
await super(KH2Context, self).shutdown()
def on_package(self, cmd: str, args: dict):
if cmd in {"RoomInfo"}:
self.kh2seedname = args['seed_name']
if not os.path.exists(self.game_communication_path):
os.makedirs(self.game_communication_path)
if not os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"):
self.kh2seedsave = {"itemIndex": -1,
# back of soras invo is 0x25E2. Growth should be moved there
# Character: [back of invo, front of invo]
"SoraInvo": [0x25D8, 0x2546],
"DonaldInvo": [0x26F4, 0x2658],
"GoofyInvo": [0x280A, 0x276C],
"AmountInvo": {
"ServerItems": {
"Ability": {},
"Amount": {},
"Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0,
"Aerial Dodge": 0,
"Glide": 0},
"Bitmask": [],
"Weapon": {"Sora": [], "Donald": [], "Goofy": []},
"Equipment": [],
"Magic": {},
"StatIncrease": {},
"Boost": {},
},
"LocalItems": {
"Ability": {},
"Amount": {},
"Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0,
"Aerial Dodge": 0, "Glide": 0},
"Bitmask": [],
"Weapon": {"Sora": [], "Donald": [], "Goofy": []},
"Equipment": [],
"Magic": {},
"StatIncrease": {},
"Boost": {},
}},
# 1,3,255 are in this list in case the player gets locations in those "worlds" and I need to still have them checked
"LocationsChecked": [],
"Levels": {
"SoraLevel": 0,
"ValorLevel": 0,
"WisdomLevel": 0,
"LimitLevel": 0,
"MasterLevel": 0,
"FinalLevel": 0,
},
"SoldEquipment": [],
"SoldBoosts": {"Power Boost": 0,
"Magic Boost": 0,
"Defense Boost": 0,
"AP Boost": 0}
}
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
'wt') as f:
pass
self.locations_checked = set()
elif os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"):
with open(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json", 'r') as f:
self.kh2seedsave = json.load(f)
self.locations_checked = set(self.kh2seedsave["LocationsChecked"])
self.serverconneced = True
if cmd in {"Connected"}:
self.kh2slotdata = args['slot_data']
self.kh2LocalItems = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()}
try:
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
logger.info("You are now auto-tracking")
self.kh2connected = True
except Exception as e:
logger.info("Line 247")
if self.kh2connected:
logger.info("Connection Lost")
self.kh2connected = False
logger.info(e)
if cmd in {"ReceivedItems"}:
start_index = args["index"]
if start_index == 0:
# resetting everything that were sent from the server
self.kh2seedsave["SoraInvo"][0] = 0x25D8
self.kh2seedsave["DonaldInvo"][0] = 0x26F4
self.kh2seedsave["GoofyInvo"][0] = 0x280A
self.kh2seedsave["itemIndex"] = - 1
self.kh2seedsave["AmountInvo"]["ServerItems"] = {
"Ability": {},
"Amount": {},
"Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0,
"Aerial Dodge": 0,
"Glide": 0},
"Bitmask": [],
"Weapon": {"Sora": [], "Donald": [], "Goofy": []},
"Equipment": [],
"Magic": {},
"StatIncrease": {},
"Boost": {},
}
if start_index > self.kh2seedsave["itemIndex"]:
self.kh2seedsave["itemIndex"] = start_index
for item in args['items']:
asyncio.create_task(self.give_item(item.item))
if cmd in {"RoomUpdate"}:
if "checked_locations" in args:
new_locations = set(args["checked_locations"])
# TODO: make this take locations from other players on the same slot so proper coop happens
# items_to_give = [self.kh2slotdata["LocalItems"][str(location_id)] for location_id in new_locations if
# location_id in self.kh2LocalItems.keys()]
self.checked_locations |= new_locations
async def checkWorldLocations(self):
try:
currentworldint = int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big")
if currentworldint in self.worldid:
curworldid = self.worldid[currentworldint]
for location, data in curworldid.items():
locationId = kh2_loc_name_to_id[location]
if locationId not in self.locations_checked \
and (int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
"big") & 0x1 << data.bitIndex) > 0:
self.sending = self.sending + [(int(locationId))]
except Exception as e:
logger.info("Line 285")
if self.kh2connected:
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
async def checkLevels(self):
try:
for location, data in SoraLevels.items():
currentLevel = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1), "big")
locationId = kh2_loc_name_to_id[location]
if locationId not in self.locations_checked \
and currentLevel >= data.bitIndex:
if self.kh2seedsave["Levels"]["SoraLevel"] < currentLevel:
self.kh2seedsave["Levels"]["SoraLevel"] = currentLevel
self.sending = self.sending + [(int(locationId))]
formDict = {
0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels],
3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels]}
for i in range(5):
for location, data in formDict[i][1].items():
formlevel = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), "big")
locationId = kh2_loc_name_to_id[location]
if locationId not in self.locations_checked \
and formlevel >= data.bitIndex:
if formlevel > self.kh2seedsave["Levels"][formDict[i][0]]:
self.kh2seedsave["Levels"][formDict[i][0]] = formlevel
self.sending = self.sending + [(int(locationId))]
except Exception as e:
logger.info("Line 312")
if self.kh2connected:
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
async def checkSlots(self):
try:
for location, data in weaponSlots.items():
locationId = kh2_loc_name_to_id[location]
if locationId not in self.locations_checked:
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
"big") > 0:
self.sending = self.sending + [(int(locationId))]
for location, data in formSlots.items():
locationId = kh2_loc_name_to_id[location]
if locationId not in self.locations_checked:
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
"big") & 0x1 << data.bitIndex > 0:
# self.locations_checked
self.sending = self.sending + [(int(locationId))]
except Exception as e:
if self.kh2connected:
logger.info("Line 333")
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
async def verifyChests(self):
try:
for location in self.locations_checked:
locationName = self.lookup_id_to_Location[location]
if locationName in self.chest_set:
if locationName in self.location_name_to_worlddata.keys():
locationData = self.location_name_to_worlddata[locationName]
if int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1),
"big") & 0x1 << locationData.bitIndex == 0:
roomData = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained,
1), "big")
self.kh2.write_bytes(self.kh2.base_address + self.Save + locationData.addrObtained,
(roomData | 0x01 << locationData.bitIndex).to_bytes(1, 'big'), 1)
except Exception as e:
if self.kh2connected:
logger.info("Line 350")
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
async def verifyLevel(self):
for leveltype, anchor in {"SoraLevel": 0x24FF,
"ValorLevel": 0x32F6,
"WisdomLevel": 0x332E,
"LimitLevel": 0x3366,
"MasterLevel": 0x339E,
"FinalLevel": 0x33D6}.items():
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + anchor, 1), "big") < \
self.kh2seedsave["Levels"][leveltype]:
self.kh2.write_bytes(self.kh2.base_address + self.Save + anchor,
(self.kh2seedsave["Levels"][leveltype]).to_bytes(1, 'big'), 1)
async def give_item(self, item, ItemType="ServerItems"):
try:
itemname = self.lookup_id_to_item[item]
itemcode = self.item_name_to_data[itemname]
if itemcode.ability:
abilityInvoType = 0
TwilightZone = 2
if ItemType == "LocalItems":
abilityInvoType = 1
TwilightZone = -2
if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}:
self.kh2seedsave["AmountInvo"][ItemType]["Growth"][itemname] += 1
return
if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Ability"]:
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname] = []
# appending the slot that the ability should be in
if len(self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname]) < \
self.AbilityQuantityDict[itemname]:
if itemname in self.sora_ability_set:
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
self.kh2seedsave["SoraInvo"][abilityInvoType])
self.kh2seedsave["SoraInvo"][abilityInvoType] -= TwilightZone
elif itemname in self.donald_ability_set:
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
self.kh2seedsave["DonaldInvo"][abilityInvoType])
self.kh2seedsave["DonaldInvo"][abilityInvoType] -= TwilightZone
else:
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
self.kh2seedsave["GoofyInvo"][abilityInvoType])
self.kh2seedsave["GoofyInvo"][abilityInvoType] -= TwilightZone
elif itemcode.code in self.bitmask_item_code:
if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"]:
self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"].append(itemname)
elif itemcode.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}:
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Magic"]:
self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] += 1
else:
self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] = 1
elif itemname in self.all_equipment:
self.kh2seedsave["AmountInvo"][ItemType]["Equipment"].append(itemname)
elif itemname in self.all_weapons:
if itemname in self.keyblade_set:
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Sora"].append(itemname)
elif itemname in self.staff_set:
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Donald"].append(itemname)
else:
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Goofy"].append(itemname)
elif itemname in self.boost_set:
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Boost"]:
self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] += 1
else:
self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] = 1
elif itemname in self.stat_increase_set:
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"]:
self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] += 1
else:
self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] = 1
else:
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Amount"]:
self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] += 1
else:
self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] = 1
except Exception as e:
if self.kh2connected:
logger.info("Line 398")
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
def run_gui(self):
"""Import kivy UI system and start running it as self.ui_task."""
from kvui import GameManager
class KH2Manager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago KH2 Client"
self.ui = KH2Manager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def IsInShop(self, sellable, master_boost):
# journal = 0x741230 shop = 0x741320
# if journal=-1 and shop = 5 then in shop
# if journam !=-1 and shop = 10 then journal
journal = self.kh2.read_short(self.kh2.base_address + 0x741230)
shop = self.kh2.read_short(self.kh2.base_address + 0x741320)
if (journal == -1 and shop == 5) or (journal != -1 and shop == 10):
# print("your in the shop")
sellable_dict = {}
for itemName in sellable:
itemdata = self.item_name_to_data[itemName]
amount = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big")
sellable_dict[itemName] = amount
while (journal == -1 and shop == 5) or (journal != -1 and shop == 10):
journal = self.kh2.read_short(self.kh2.base_address + 0x741230)
shop = self.kh2.read_short(self.kh2.base_address + 0x741320)
await asyncio.sleep(0.5)
for item, amount in sellable_dict.items():
itemdata = self.item_name_to_data[item]
afterShop = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big")
if afterShop < amount:
if item in master_boost:
self.kh2seedsave["SoldBoosts"][item] += (amount - afterShop)
else:
self.kh2seedsave["SoldEquipment"].append(item)
async def verifyItems(self):
try:
local_amount = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"].keys())
server_amount = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"].keys())
master_amount = local_amount | server_amount
local_ability = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"].keys())
server_ability = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"].keys())
master_ability = local_ability | server_ability
local_bitmask = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Bitmask"])
server_bitmask = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Bitmask"])
master_bitmask = local_bitmask | server_bitmask
local_keyblade = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Sora"])
local_staff = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Donald"])
local_shield = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Goofy"])
server_keyblade = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Sora"])
server_staff = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Donald"])
server_shield = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Goofy"])
master_keyblade = local_keyblade | server_keyblade
master_staff = local_staff | server_staff
master_shield = local_shield | server_shield
local_equipment = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Equipment"])
server_equipment = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Equipment"])
master_equipment = local_equipment | server_equipment
local_magic = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"].keys())
server_magic = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"].keys())
master_magic = local_magic | server_magic
local_stat = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"].keys())
server_stat = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"].keys())
master_stat = local_stat | server_stat
local_boost = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"].keys())
server_boost = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"].keys())
master_boost = local_boost | server_boost
master_sell = master_equipment | master_staff | master_shield | master_boost
await asyncio.create_task(self.IsInShop(master_sell, master_boost))
for itemName in master_amount:
itemData = self.item_name_to_data[itemName]
amountOfItems = 0
if itemName in local_amount:
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"][itemName]
if itemName in server_amount:
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"][itemName]
if itemName == "Torn Page":
# Torn Pages are handled differently because they can be consumed.
# Will check the progression in 100 acre and - the amount of visits
# amountofitems-amount of visits done
for location, data in tornPageLocks.items():
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
"big") & 0x1 << data.bitIndex > 0:
amountOfItems -= 1
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != amountOfItems and amountOfItems >= 0:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
amountOfItems.to_bytes(1, 'big'), 1)
for itemName in master_keyblade:
itemData = self.item_name_to_data[itemName]
# if the inventory slot for that keyblade is less than the amount they should have
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != 1 and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x1CFF, 1),
"big") != 13:
# Checking form anchors for the keyblade
if self.kh2.read_short(self.kh2.base_address + self.Save + 0x24F0) == itemData.kh2id \
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x32F4) == itemData.kh2id \
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x339C) == itemData.kh2id \
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x33D4) == itemData.kh2id:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(0).to_bytes(1, 'big'), 1)
else:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(1).to_bytes(1, 'big'), 1)
for itemName in master_staff:
itemData = self.item_name_to_data[itemName]
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != 1 \
and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2604) != itemData.kh2id \
and itemName not in self.kh2seedsave["SoldEquipment"]:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(1).to_bytes(1, 'big'), 1)
for itemName in master_shield:
itemData = self.item_name_to_data[itemName]
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != 1 \
and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2718) != itemData.kh2id \
and itemName not in self.kh2seedsave["SoldEquipment"]:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(1).to_bytes(1, 'big'), 1)
for itemName in master_ability:
itemData = self.item_name_to_data[itemName]
ability_slot = []
if itemName in local_ability:
ability_slot += self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"][itemName]
if itemName in server_ability:
ability_slot += self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"][itemName]
for slot in ability_slot:
current = self.kh2.read_short(self.kh2.base_address + self.Save + slot)
ability = current & 0x0FFF
if ability | 0x8000 != (0x8000 + itemData.memaddr):
if current - 0x8000 > 0:
self.kh2.write_short(self.kh2.base_address + self.Save + slot, (0x8000 + itemData.memaddr))
else:
self.kh2.write_short(self.kh2.base_address + self.Save + slot, itemData.memaddr)
# removes the duped ability if client gave faster than the game.
for charInvo in {"SoraInvo", "DonaldInvo", "GoofyInvo"}:
if self.kh2.read_short(self.kh2.base_address + self.Save + self.kh2seedsave[charInvo][1]) != 0 and \
self.kh2seedsave[charInvo][1] + 2 < self.kh2seedsave[charInvo][0]:
self.kh2.write_short(self.kh2.base_address + self.Save + self.kh2seedsave[charInvo][1], 0)
# remove the dummy level 1 growths if they are in these invo slots.
for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}:
current = self.kh2.read_short(self.kh2.base_address + self.Save + inventorySlot)
ability = current & 0x0FFF
if 0x05E <= ability <= 0x06D:
self.kh2.write_short(self.kh2.base_address + self.Save + inventorySlot, 0)
for itemName in self.master_growth:
growthLevel = self.kh2seedsave["AmountInvo"]["ServerItems"]["Growth"][itemName] \
+ self.kh2seedsave["AmountInvo"]["LocalItems"]["Growth"][itemName]
if growthLevel > 0:
slot = self.growth_values_dict[itemName][2]
min_growth = self.growth_values_dict[itemName][0]
max_growth = self.growth_values_dict[itemName][1]
if growthLevel > 4:
growthLevel = 4
current_growth_level = self.kh2.read_short(self.kh2.base_address + self.Save + slot)
ability = current_growth_level & 0x0FFF
# if the player should be getting a growth ability
if ability | 0x8000 != 0x8000 + min_growth - 1 + growthLevel:
# if it should be level one of that growth
if 0x8000 + min_growth - 1 + growthLevel <= 0x8000 + min_growth or ability < min_growth:
self.kh2.write_short(self.kh2.base_address + self.Save + slot, min_growth)
# if it is already in the inventory
elif ability | 0x8000 < (0x8000 + max_growth):
self.kh2.write_short(self.kh2.base_address + self.Save + slot, current_growth_level + 1)
for itemName in master_bitmask:
itemData = self.item_name_to_data[itemName]
itemMemory = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), "big")
if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") & 0x1 << itemData.bitmask) == 0:
# when getting a form anti points should be reset to 0 but bit-shift doesn't trigger the game.
if itemName in {"Valor Form", "Wisdom Form", "Limit Form", "Master Form", "Final Form"}:
self.kh2.write_bytes(self.kh2.base_address + self.Save + 0x3410,
(0).to_bytes(1, 'big'), 1)
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(itemMemory | 0x01 << itemData.bitmask).to_bytes(1, 'big'), 1)
for itemName in master_equipment:
itemData = self.item_name_to_data[itemName]
isThere = False
if itemName in self.accessories_set:
Equipment_Anchor_List = self.Equipment_Anchor_Dict["Accessories"]
else:
Equipment_Anchor_List = self.Equipment_Anchor_Dict["Armor"]
# Checking form anchors for the equipment
for slot in Equipment_Anchor_List:
if self.kh2.read_short(self.kh2.base_address + self.Save + slot) == itemData.kh2id:
isThere = True
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != 0:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(0).to_bytes(1, 'big'), 1)
break
if not isThere and itemName not in self.kh2seedsave["SoldEquipment"]:
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != 1:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(1).to_bytes(1, 'big'), 1)
for itemName in master_magic:
itemData = self.item_name_to_data[itemName]
amountOfItems = 0
if itemName in local_magic:
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"][itemName]
if itemName in server_magic:
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"][itemName]
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != amountOfItems \
and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x741320, 1), "big") in {10, 8}:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
amountOfItems.to_bytes(1, 'big'), 1)
for itemName in master_stat:
itemData = self.item_name_to_data[itemName]
amountOfItems = 0
if itemName in local_stat:
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"][itemName]
if itemName in server_stat:
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"][itemName]
# 0x130293 is Crit_1's location id for touching the computer
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != amountOfItems \
and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Slot1 + 0x1B2, 1),
"big") >= 5 and int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x23DF, 1),
"big") > 0:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
amountOfItems.to_bytes(1, 'big'), 1)
for itemName in master_boost:
itemData = self.item_name_to_data[itemName]
amountOfItems = 0
if itemName in local_boost:
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"][itemName]
if itemName in server_boost:
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"][itemName]
amountOfBoostsInInvo = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big")
amountOfUsedBoosts = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + self.boost_to_anchor_dict[itemName], 1),
"big")
# Ap Boots start at +50 for some reason
if itemName == "AP Boost":
amountOfUsedBoosts -= 50
totalBoosts = (amountOfBoostsInInvo + amountOfUsedBoosts)
if totalBoosts <= amountOfItems - self.kh2seedsave["SoldBoosts"][
itemName] and amountOfBoostsInInvo < 255:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(amountOfBoostsInInvo + 1).to_bytes(1, 'big'), 1)
except Exception as e:
logger.info("Line 573")
if self.kh2connected:
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
def finishedGame(ctx: KH2Context, message):
if ctx.kh2slotdata['FinalXemnas'] == 1:
if 0x1301ED in message[0]["locations"]:
ctx.finalxemnas = True
# three proofs
if ctx.kh2slotdata['Goal'] == 0:
if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, 1), "big") > 0 \
and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, 1), "big") > 0 \
and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, 1), "big") > 0:
if ctx.kh2slotdata['FinalXemnas'] == 1:
if ctx.finalxemnas:
return True
else:
return False
else:
return True
else:
return False
elif ctx.kh2slotdata['Goal'] == 1:
if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x3641, 1), "big") >= \
ctx.kh2slotdata['LuckyEmblemsRequired']:
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1)
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1)
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1)
if ctx.kh2slotdata['FinalXemnas'] == 1:
if ctx.finalxemnas:
return True
else:
return False
else:
return True
else:
return False
elif ctx.kh2slotdata['Goal'] == 2:
for boss in ctx.kh2slotdata["hitlist"]:
if boss in message[0]["locations"]:
ctx.amountOfPieces += 1
if ctx.amountOfPieces >= ctx.kh2slotdata["BountyRequired"]:
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1)
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1)
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1)
if ctx.kh2slotdata['FinalXemnas'] == 1:
if ctx.finalxemnas:
return True
else:
return False
else:
return True
else:
return False
async def kh2_watcher(ctx: KH2Context):
while not ctx.exit_event.is_set():
try:
if ctx.kh2connected and ctx.serverconneced:
ctx.sending = []
await asyncio.create_task(ctx.checkWorldLocations())
await asyncio.create_task(ctx.checkLevels())
await asyncio.create_task(ctx.checkSlots())
await asyncio.create_task(ctx.verifyChests())
await asyncio.create_task(ctx.verifyItems())
await asyncio.create_task(ctx.verifyLevel())
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
if finishedGame(ctx, message):
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
location_ids = []
location_ids = [location for location in message[0]["locations"] if location not in location_ids]
for location in location_ids:
if location not in ctx.locations_checked:
ctx.locations_checked.add(location)
ctx.kh2seedsave["LocationsChecked"].append(location)
if location in ctx.kh2LocalItems:
item = ctx.kh2slotdata["LocalItems"][str(location)]
await asyncio.create_task(ctx.give_item(item, "LocalItems"))
await ctx.send_msgs(message)
elif not ctx.kh2connected and ctx.serverconneced:
logger.info("Game is not open. Disconnecting from Server.")
await ctx.disconnect()
except Exception as e:
logger.info("Line 661")
if ctx.kh2connected:
logger.info("Connection Lost.")
ctx.kh2connected = False
logger.info(e)
await asyncio.sleep(0.5)
if __name__ == '__main__':
async def main(args):
ctx = KH2Context(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
progression_watcher = asyncio.create_task(
kh2_watcher(ctx), name="KH2ProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
await progression_watcher
await ctx.shutdown()
import colorama
parser = get_base_parser(description="KH2 Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -11,13 +11,18 @@ Scroll down to components= to add components to the launcher as well as setup.py
import argparse
import itertools
import logging
import multiprocessing
import shlex
import subprocess
import sys
from enum import Enum, auto
import webbrowser
from os.path import isfile
from shutil import which
from typing import Iterable, Sequence, Callable, Union, Optional
from typing import Sequence, Union, Optional
import Utils
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths
if __name__ == "__main__":
import ModuleUpdate
@@ -37,7 +42,6 @@ def open_host_yaml():
exe = which("open")
subprocess.Popen([exe, file])
else:
import webbrowser
webbrowser.open(file)
@@ -52,121 +56,54 @@ def open_patch():
except Exception as e:
messagebox('Error', str(e), error=True)
else:
file, _, component = identify(filename)
file, component = identify(filename)
if file and component:
launch([*get_exe(component), file], component.cli)
def generate_yamls():
from Options import generate_yaml_templates
target = Utils.user_path("Players", "Templates")
generate_yaml_templates(target, False)
open_folder(target)
def browse_files():
file = user_path()
open_folder(user_path())
def open_folder(folder_path):
if is_linux:
exe = which('xdg-open') or which('gnome-open') or which('kde-open')
subprocess.Popen([exe, file])
subprocess.Popen([exe, folder_path])
elif is_macos:
exe = which("open")
subprocess.Popen([exe, file])
subprocess.Popen([exe, folder_path])
else:
import webbrowser
webbrowser.open(file)
webbrowser.open(folder_path)
# noinspection PyArgumentList
class Type(Enum):
TOOL = auto()
FUNC = auto() # not a real component
CLIENT = auto()
ADJUSTER = auto()
class SuffixIdentifier:
suffixes: Iterable[str]
def __init__(self, *args: str):
self.suffixes = args
def __call__(self, path: str):
if isinstance(path, str):
for suffix in self.suffixes:
if path.endswith(suffix):
return True
return False
class Component:
display_name: str
type: Optional[Type]
script_name: Optional[str]
frozen_name: Optional[str]
icon: str # just the name, no suffix
cli: bool
func: Optional[Callable]
file_identifier: Optional[Callable[[str], bool]]
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
cli: bool = False, icon: str = 'icon', component_type: Type = None, func: Optional[Callable] = None,
file_identifier: Optional[Callable[[str], bool]] = None):
self.display_name = display_name
self.script_name = script_name
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
self.icon = icon
self.cli = cli
self.type = component_type or \
None if not display_name else \
Type.FUNC if func else \
Type.CLIENT if 'Client' in display_name else \
Type.ADJUSTER if 'Adjuster' in display_name else Type.TOOL
self.func = func
self.file_identifier = file_identifier
def handles_file(self, path: str):
return self.file_identifier(path) if self.file_identifier else False
components: Iterable[Component] = (
# Launcher
Component('', 'Launcher'),
# Core
Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True,
file_identifier=SuffixIdentifier('.archipelago', '.zip')),
Component('Generate', 'Generate', cli=True),
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'),
# SNI
Component('SNI Client', 'SNIClient',
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3')),
Component('LttP Adjuster', 'LttPAdjuster'),
# Factorio
Component('Factorio Client', 'FactorioClient'),
# Minecraft
Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True,
file_identifier=SuffixIdentifier('.apmc')),
# Ocarina of Time
Component('OoT Client', 'OoTClient',
file_identifier=SuffixIdentifier('.apz5')),
Component('OoT Adjuster', 'OoTAdjuster'),
# FF1
Component('FF1 Client', 'FF1Client'),
# ChecksFinder
Component('ChecksFinder Client', 'ChecksFinderClient'),
# Starcraft 2
Component('Starcraft 2 Client', 'Starcraft2Client'),
components.extend([
# Functions
Component('Open host.yaml', func=open_host_yaml),
Component('Open Patch', func=open_patch),
Component('Browse Files', func=browse_files),
)
icon_paths = {
'icon': local_path('data', 'icon.ico' if is_windows else 'icon.png'),
'mcicon': local_path('data', 'mcicon.ico')
}
Component("Open host.yaml", func=open_host_yaml),
Component("Open Patch", func=open_patch),
Component("Generate Template Settings", func=generate_yamls),
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
Component("18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
Component("Browse Files", func=browse_files),
])
def identify(path: Union[None, str]):
if path is None:
return None, None, None
return None, None
for component in components:
if component.handles_file(path):
return path, component.script_name, component
return (None, None, None) if '/' in path or '\\' in path else (None, path, None)
return path, component
elif path == component.display_name or path == component.script_name:
return None, component
return None, None
def get_exe(component: Union[str, Component]) -> Optional[Sequence[str]]:
@@ -213,16 +150,18 @@ def launch(exe, in_terminal=False):
def run_gui():
from kvui import App, ContainerLayout, GridLayout, Button, Label
from kivy.uix.image import AsyncImage
from kivy.uix.relativelayout import RelativeLayout
class Launcher(App):
base_title: str = "Archipelago Launcher"
container: ContainerLayout
grid: GridLayout
_tools = {c.display_name: c for c in components if c.type == Type.TOOL and isfile(get_exe(c)[-1])}
_clients = {c.display_name: c for c in components if c.type == Type.CLIENT and isfile(get_exe(c)[-1])}
_adjusters = {c.display_name: c for c in components if c.type == Type.ADJUSTER and isfile(get_exe(c)[-1])}
_funcs = {c.display_name: c for c in components if c.type == Type.FUNC}
_tools = {c.display_name: c for c in components if c.type == Type.TOOL}
_clients = {c.display_name: c for c in components if c.type == Type.CLIENT}
_adjusters = {c.display_name: c for c in components if c.type == Type.ADJUSTER}
_miscs = {c.display_name: c for c in components if c.type == Type.MISC}
def __init__(self, ctx=None):
self.title = self.base_title
@@ -234,24 +173,44 @@ def run_gui():
self.container = ContainerLayout()
self.grid = GridLayout(cols=2)
self.container.add_widget(self.grid)
self.grid.add_widget(Label(text="General"))
self.grid.add_widget(Label(text="Clients"))
button_layout = self.grid # make buttons fill the window
def build_button(component: Component):
"""
Builds a button widget for a given component.
Args:
component (Component): The component associated with the button.
Returns:
None. The button is added to the parent grid layout.
"""
button = Button(text=component.display_name)
button.component = component
button.bind(on_release=self.component_action)
if component.icon != "icon":
image = AsyncImage(source=icon_paths[component.icon],
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
box_layout = RelativeLayout()
box_layout.add_widget(button)
box_layout.add_widget(image)
button_layout.add_widget(box_layout)
else:
button_layout.add_widget(button)
for (tool, client) in itertools.zip_longest(itertools.chain(
self._tools.items(), self._funcs.items(), self._adjusters.items()), self._clients.items()):
self._tools.items(), self._miscs.items(), self._adjusters.items()), self._clients.items()):
# column 1
if tool:
button = Button(text=tool[0])
button.component = tool[1]
button.bind(on_release=self.component_action)
button_layout.add_widget(button)
build_button(tool[1])
else:
button_layout.add_widget(Label())
# column 2
if client:
button = Button(text=client[0])
button.component = client[1]
button.bind(on_press=self.component_action)
button_layout.add_widget(button)
build_button(client[1])
else:
button_layout.add_widget(Label())
@@ -259,14 +218,29 @@ def run_gui():
@staticmethod
def component_action(button):
if button.component.type == Type.FUNC:
if button.component.func:
button.component.func()
else:
launch(get_exe(button.component), button.component.cli)
def _stop(self, *largs):
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.
# Closing the window explicitly cleans it up.
self.root_window.close()
super()._stop(*largs)
Launcher().run()
def run_component(component: Component, *args):
if component.func:
component.func(*args)
elif component.script_name:
subprocess.run([*get_exe(component.script_name), *args])
else:
logging.warning(f"Component {component} does not appear to be executable.")
def main(args: Optional[Union[argparse.Namespace, dict]] = None):
if isinstance(args, argparse.Namespace):
args = {k: v for k, v in args._get_kwargs()}
@@ -274,24 +248,34 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
args = {}
if "Patch|Game|Component" in args:
file, component, _ = identify(args["Patch|Game|Component"])
file, component = identify(args["Patch|Game|Component"])
if file:
args['file'] = file
if component:
args['component'] = component
if not component:
logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}")
if 'file' in args:
subprocess.run([*get_exe(args['component']), args['file'], *args['args']])
run_component(args["component"], args["file"], *args["args"])
elif 'component' in args:
subprocess.run([*get_exe(args['component']), *args['args']])
run_component(args["component"], *args["args"])
else:
run_gui()
if __name__ == '__main__':
init_logging('Launcher')
Utils.freeze_support()
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
parser = argparse.ArgumentParser(description='Archipelago Launcher')
parser.add_argument('Patch|Game|Component', type=str, nargs='?',
help="Pass either a patch file, a generated game or the name of a component to run.")
parser.add_argument('args', nargs="*", help="Arguments to pass to component.")
main(parser.parse_args())
from worlds.LauncherComponents import processes
for process in processes:
# we await all child processes to close before we tear down the process host
# this makes it feel like each one is its own program, as the Launcher is closed now
process.join()

613
LinksAwakeningClient.py Normal file
View File

@@ -0,0 +1,613 @@
import ModuleUpdate
ModuleUpdate.update()
import Utils
if __name__ == "__main__":
Utils.init_logging("LinksAwakeningContext", exception_logger="Client")
import asyncio
import base64
import binascii
import io
import logging
import select
import socket
import time
import typing
import urllib
import colorama
import struct
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
server_loop)
from NetUtils import ClientStatus
from worlds.ladx.Common import BASE_ID as LABaseID
from worlds.ladx.GpsTracker import GpsTracker
from worlds.ladx.ItemTracker import ItemTracker
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
from worlds.ladx.Tracker import LocationTracker, MagpieBridge
class GameboyException(Exception):
pass
class RetroArchDisconnectError(GameboyException):
pass
class InvalidEmulatorStateError(GameboyException):
pass
class BadRetroArchResponse(GameboyException):
pass
def magpie_logo():
from kivy.uix.image import CoreImage
binary_data = """
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXN
SR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA
7DAcdvqGQAAADGSURBVDhPhVLBEcIwDHOYhjHCBuXHj2OTbAL8+
MEGZIxOQ1CinOOk0Op0bmo7tlXXeR9FJMYDLOD9mwcLjQK7+hSZ
wgcWMZJOAGeGKtChNHFL0j+FZD3jSCuo0w7l03wDrWdg00C4/aW
eDEYNenuzPOfPspBnxf0kssE80vN0L8361j10P03DK4x6FHabuV
ear8fHme+b17rwSjbAXeUMLb+EVTV2QHm46MWQanmnydA98KsVS
XkV+qFpGQXrLhT/fqraQeQLuplpNH5g+WkAAAAASUVORK5CYII="""
binary_data = base64.b64decode(binary_data)
data = io.BytesIO(binary_data)
return CoreImage(data, ext="png").texture
class LAClientConstants:
# Connector version
VERSION = 0x01
#
# Memory locations of LADXR
ROMGameID = 0x0051 # 4 bytes
SlotName = 0x0134
# Unused
# ROMWorldID = 0x0055
# ROMConnectorVersion = 0x0056
# RO: We should only act if this is higher then 6, as it indicates that the game is running normally
wGameplayType = 0xDB95
# RO: Starts at 0, increases every time an item is received from the server and processed
wLinkSyncSequenceNumber = 0xDDF6
wLinkStatusBits = 0xDDF7 # RW:
# Bit0: wLinkGive* contains valid data, set from script cleared from ROM.
wLinkHealth = 0xDB5A
wLinkGiveItem = 0xDDF8 # RW
wLinkGiveItemFrom = 0xDDF9 # RW
# All of these six bytes are unused, we can repurpose
# wLinkSendItemRoomHigh = 0xDDFA # RO
# wLinkSendItemRoomLow = 0xDDFB # RO
# wLinkSendItemTarget = 0xDDFC # RO
# wLinkSendItemItem = 0xDDFD # RO
# wLinkSendShopItem = 0xDDFE # RO, which item to send (1 based, order of the shop items)
# RO, which player to send to, but it's just the X position of the NPC used, so 0x18 is player 0
# wLinkSendShopTarget = 0xDDFF
wRecvIndex = 0xDDFD # Two bytes
wCheckAddress = 0xC0FF - 0x4
WRamCheckSize = 0x4
WRamSafetyValue = bytearray([0]*WRamCheckSize)
MinGameplayValue = 0x06
MaxGameplayValue = 0x1A
VictoryGameplayAndSub = 0x0102
class RAGameboy():
cache = []
cache_start = 0
cache_size = 0
last_cache_read = None
socket = None
def __init__(self, address, port) -> None:
self.address = address
self.port = port
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
assert (self.socket)
self.socket.setblocking(False)
def get_retroarch_version(self):
self.send(b'VERSION\n')
select.select([self.socket], [], [])
response_str, addr = self.socket.recvfrom(16)
return response_str.rstrip()
def get_retroarch_status(self, timeout):
self.send(b'GET_STATUS\n')
select.select([self.socket], [], [], timeout)
response_str, addr = self.socket.recvfrom(1000, )
return response_str.rstrip()
def set_cache_limits(self, cache_start, cache_size):
self.cache_start = cache_start
self.cache_size = cache_size
def send(self, b):
if type(b) is str:
b = b.encode('ascii')
self.socket.sendto(b, (self.address, self.port))
def recv(self):
select.select([self.socket], [], [])
response, _ = self.socket.recvfrom(4096)
return response
async def async_recv(self):
response = await asyncio.get_event_loop().sock_recv(self.socket, 4096)
return response
async def check_safe_gameplay(self, throw=True):
async def check_wram():
check_values = await self.async_read_memory(LAClientConstants.wCheckAddress, LAClientConstants.WRamCheckSize)
if check_values != LAClientConstants.WRamSafetyValue:
if throw:
raise InvalidEmulatorStateError()
return False
return True
if not await check_wram():
if throw:
raise InvalidEmulatorStateError()
return False
gameplay_value = await self.async_read_memory(LAClientConstants.wGameplayType)
gameplay_value = gameplay_value[0]
# In gameplay or credits
if not (LAClientConstants.MinGameplayValue <= gameplay_value <= LAClientConstants.MaxGameplayValue) and gameplay_value != 0x1:
if throw:
logger.info("invalid emu state")
raise InvalidEmulatorStateError()
return False
if not await check_wram():
return False
return True
# We're sadly unable to update the whole cache at once
# as RetroArch only gives back some number of bytes at a time
# So instead read as big as chunks at a time as we can manage
async def update_cache(self):
# First read the safety address - if it's invalid, bail
self.cache = []
if not await self.check_safe_gameplay():
return
cache = []
remaining_size = self.cache_size
while remaining_size:
block = await self.async_read_memory(self.cache_start + len(cache), remaining_size)
remaining_size -= len(block)
cache += block
if not await self.check_safe_gameplay():
return
self.cache = cache
self.last_cache_read = time.time()
async def read_memory_cache(self, addresses):
# TODO: can we just update once per frame?
if not self.last_cache_read or self.last_cache_read + 0.1 < time.time():
await self.update_cache()
if not self.cache:
return None
assert (len(self.cache) == self.cache_size)
for address in addresses:
assert self.cache_start <= address <= self.cache_start + self.cache_size
r = {address: self.cache[address - self.cache_start]
for address in addresses}
return r
async def async_read_memory_safe(self, address, size=1):
# whenever we do a read for a check, we need to make sure that we aren't reading
# garbage memory values - we also need to protect against reading a value, then the emulator resetting
#
# ...actually, we probably _only_ need the post check
# Check before read
if not await self.check_safe_gameplay():
return None
# Do read
r = await self.async_read_memory(address, size)
# Check after read
if not await self.check_safe_gameplay():
return None
return r
def read_memory(self, address, size=1):
command = "READ_CORE_MEMORY"
self.send(f'{command} {hex(address)} {size}\n')
response = self.recv()
splits = response.decode().split(" ", 2)
assert (splits[0] == command)
# Ignore the address for now
# TODO: transform to bytes
if splits[2][:2] == "-1" or splits[0] != "READ_CORE_MEMORY":
raise BadRetroArchResponse()
return bytearray.fromhex(splits[2])
async def async_read_memory(self, address, size=1):
command = "READ_CORE_MEMORY"
self.send(f'{command} {hex(address)} {size}\n')
response = await self.async_recv()
response = response[:-1]
splits = response.decode().split(" ", 2)
assert (splits[0] == command)
# Ignore the address for now
# TODO: transform to bytes
return bytearray.fromhex(splits[2])
def write_memory(self, address, bytes):
command = "WRITE_CORE_MEMORY"
self.send(f'{command} {hex(address)} {" ".join(hex(b) for b in bytes)}')
select.select([self.socket], [], [])
response, _ = self.socket.recvfrom(4096)
splits = response.decode().split(" ", 3)
assert (splits[0] == command)
if splits[2] == "-1":
logger.info(splits[3])
class LinksAwakeningClient():
socket = None
gameboy = None
tracker = None
auth = None
game_crc = None
pending_deathlink = False
deathlink_debounce = True
recvd_checks = {}
def msg(self, m):
logger.info(m)
s = f"SHOW_MSG {m}\n"
self.gameboy.send(s)
def __init__(self, retroarch_address="127.0.0.1", retroarch_port=55355):
self.gameboy = RAGameboy(retroarch_address, retroarch_port)
async def wait_for_retroarch_connection(self):
logger.info("Waiting on connection to Retroarch...")
while True:
try:
version = self.gameboy.get_retroarch_version()
NO_CONTENT = b"GET_STATUS CONTENTLESS"
status = NO_CONTENT
core_type = None
GAME_BOY = b"game_boy"
while status == NO_CONTENT or core_type != GAME_BOY:
try:
status = self.gameboy.get_retroarch_status(0.1)
if status.count(b" ") < 2:
await asyncio.sleep(1.0)
continue
GET_STATUS, PLAYING, info = status.split(b" ", 2)
if status.count(b",") < 2:
await asyncio.sleep(1.0)
continue
core_type, rom_name, self.game_crc = info.split(b",", 2)
if core_type != GAME_BOY:
logger.info(
f"Core type should be '{GAME_BOY}', found {core_type} instead - wrong type of ROM?")
await asyncio.sleep(1.0)
continue
except (BlockingIOError, TimeoutError) as e:
await asyncio.sleep(0.1)
pass
logger.info(f"Connected to Retroarch {version} {info}")
self.gameboy.read_memory(0x1000)
return
except ConnectionResetError:
await asyncio.sleep(1.0)
pass
def reset_auth(self):
auth = binascii.hexlify(self.gameboy.read_memory(0x0134, 12)).decode()
if self.auth:
assert (auth == self.auth)
self.auth = auth
async def wait_and_init_tracker(self):
await self.wait_for_game_ready()
self.tracker = LocationTracker(self.gameboy)
self.item_tracker = ItemTracker(self.gameboy)
self.gps_tracker = GpsTracker(self.gameboy)
async def recved_item_from_ap(self, item_id, from_player, next_index):
# Don't allow getting an item until you've got your first check
if not self.tracker.has_start_item():
return
# Spin until we either:
# get an exception from a bad read (emu shut down or reset)
# beat the game
# the client handles the last pending item
status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0]
while not (await self.is_victory()) and status & 1 == 1:
time.sleep(0.1)
status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0]
item_id -= LABaseID
# The player name table only goes up to 100, so don't go past that
# Even if it didn't, the remote player _index_ byte is just a byte, so 255 max
if from_player > 100:
from_player = 100
next_index += 1
self.gameboy.write_memory(LAClientConstants.wLinkGiveItem, [
item_id, from_player])
status |= 1
status = self.gameboy.write_memory(LAClientConstants.wLinkStatusBits, [status])
self.gameboy.write_memory(LAClientConstants.wRecvIndex, struct.pack(">H", next_index))
async def wait_for_game_ready(self):
logger.info("Waiting on game to be in valid state...")
while not await self.gameboy.check_safe_gameplay(throw=False):
pass
logger.info("Ready!")
async def is_victory(self):
return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1
async def main_tick(self, item_get_cb, win_cb, deathlink_cb):
await self.tracker.readChecks(item_get_cb)
await self.item_tracker.readItems()
await self.gps_tracker.read_location()
current_health = (await self.gameboy.read_memory_cache([LAClientConstants.wLinkHealth]))[LAClientConstants.wLinkHealth]
if self.deathlink_debounce and current_health != 0:
self.deathlink_debounce = False
elif not self.deathlink_debounce and current_health == 0:
# logger.info("YOU DIED.")
await deathlink_cb()
self.deathlink_debounce = True
if self.pending_deathlink:
logger.info("Got a deathlink")
self.gameboy.write_memory(LAClientConstants.wLinkHealth, [0])
self.pending_deathlink = False
self.deathlink_debounce = True
if await self.is_victory():
await win_cb()
recv_index = struct.unpack(">H", self.gameboy.read_memory(LAClientConstants.wRecvIndex, 2))[0]
# Play back one at a time
if recv_index in self.recvd_checks:
item = self.recvd_checks[recv_index]
await self.recved_item_from_ap(item.item, item.player, recv_index)
all_tasks = set()
def create_task_log_exception(awaitable) -> asyncio.Task:
async def _log_exception(awaitable):
try:
return await awaitable
except Exception as e:
logger.exception(e)
pass
finally:
all_tasks.remove(task)
task = asyncio.create_task(_log_exception(awaitable))
all_tasks.add(task)
class LinksAwakeningContext(CommonContext):
tags = {"AP"}
game = "Links Awakening DX"
items_handling = 0b101
want_slot_data = True
la_task = None
client = None
# TODO: does this need to re-read on reset?
found_checks = []
last_resend = time.time()
magpie_enabled = False
magpie = None
magpie_task = None
won = False
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
self.client = LinksAwakeningClient()
if magpie:
self.magpie_enabled = True
self.magpie = MagpieBridge()
super().__init__(server_address, password)
def run_gui(self) -> None:
import webbrowser
import kvui
from kvui import Button, GameManager
from kivy.uix.image import Image
class LADXManager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
("Tracker", "Tracker"),
]
base_title = "Archipelago Links Awakening DX Client"
def build(self):
b = super().build()
if self.ctx.magpie_enabled:
button = Button(text="", size=(30, 30), size_hint_x=None,
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
image = Image(size=(16, 16), texture=magpie_logo())
button.add_widget(image)
def set_center(_, center):
image.center = center
button.bind(center=set_center)
self.connect_layout.add_widget(button)
return b
self.ui = LADXManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def send_checks(self):
message = [{"cmd": 'LocationChecks', "locations": self.found_checks}]
await self.send_msgs(message)
ENABLE_DEATHLINK = False
async def send_deathlink(self):
if self.ENABLE_DEATHLINK:
message = [{"cmd": 'Deathlink',
'time': time.time(),
'cause': 'Had a nightmare',
# 'source': self.slot_info[self.slot].name,
}]
await self.send_msgs(message)
async def send_victory(self):
if not self.won:
message = [{"cmd": "StatusUpdate",
"status": ClientStatus.CLIENT_GOAL}]
logger.info("victory!")
await self.send_msgs(message)
self.won = True
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
if self.ENABLE_DEATHLINK:
self.client.pending_deathlink = True
def new_checks(self, item_ids, ladxr_ids):
self.found_checks += item_ids
create_task_log_exception(self.send_checks())
if self.magpie_enabled:
create_task_log_exception(self.magpie.send_new_checks(ladxr_ids))
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(LinksAwakeningContext, self).server_auth(password_requested)
self.auth = self.client.auth
await self.get_username()
await self.send_connect()
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
self.game = self.slot_info[self.slot].game
# TODO - use watcher_event
if cmd == "ReceivedItems":
for index, item in enumerate(args["items"], args["index"]):
self.client.recvd_checks[index] = item
item_id_lookup = get_locations_to_id()
async def run_game_loop(self):
def on_item_get(ladxr_checks):
checks = [self.item_id_lookup[meta_to_name(
checkMetadataTable[check.id])] for check in ladxr_checks]
self.new_checks(checks, [check.id for check in ladxr_checks])
async def victory():
await self.send_victory()
async def deathlink():
await self.send_deathlink()
if self.magpie_enabled:
self.magpie_task = asyncio.create_task(self.magpie.serve())
# yield to allow UI to start
await asyncio.sleep(0)
while True:
try:
# TODO: cancel all client tasks
logger.info("(Re)Starting game loop")
self.found_checks.clear()
await self.client.wait_for_retroarch_connection()
self.client.reset_auth()
await self.client.wait_and_init_tracker()
while True:
await self.client.main_tick(on_item_get, victory, deathlink)
await asyncio.sleep(0.1)
now = time.time()
if self.last_resend + 5.0 < now:
self.last_resend = now
await self.send_checks()
if self.magpie_enabled:
self.magpie.set_checks(self.client.tracker.all_checks)
await self.magpie.set_item_tracker(self.client.item_tracker)
await self.magpie.send_gps(self.client.gps_tracker)
except GameboyException:
time.sleep(1.0)
pass
async def main():
parser = get_base_parser(description="Link's Awakening Client.")
parser.add_argument("--url", help="Archipelago connection url")
parser.add_argument("--no-magpie", dest='magpie', default=True, action='store_false', help="Disable magpie bridge")
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a .apladx Archipelago Binary Patch file')
args = parser.parse_args()
logger.info(args)
if args.diff_file:
import Patch
logger.info("patch file was supplied - creating rom...")
meta, rom_file = Patch.create_rom_file(args.diff_file)
if "server" in meta:
args.url = meta["server"]
logger.info(f"wrote rom file to {rom_file}")
if args.url:
url = urllib.parse.urlparse(args.url)
args.connect = url.netloc
if url.password:
args.password = urllib.parse.unquote(url.password)
ctx = LinksAwakeningContext(args.connect, args.password, args.magpie)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
# TODO: nothing about the lambda about has to be in a lambda
ctx.la_task = create_task_log_exception(ctx.run_game_loop())
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
await ctx.exit_event.wait()
await ctx.shutdown()
if __name__ == '__main__':
colorama.init()
asyncio.run(main())
colorama.deinit()

View File

@@ -26,14 +26,16 @@ ModuleUpdate.update()
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
from Utils import output_path, local_path, user_path, open_file, get_cert_none_ssl_context, persistent_store, \
get_adjuster_settings, tkinter_center_window, init_logging
from Patch import GAME_ALTTP
GAME_ALTTP = "A Link to the Past"
class AdjusterWorld(object):
def __init__(self, sprite_pool):
import random
self.sprite_pool = {1: sprite_pool}
self.slot_seeds = {1: random}
self.per_slot_randoms = {1: random}
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
@@ -42,7 +44,7 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
return textwrap.dedent(action.help)
def main():
def get_argparser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('rom', nargs="?", default='AP_LttP.sfc', help='Path to an ALttP rom to adjust.')
@@ -83,9 +85,6 @@ def main():
parser.add_argument('--ow_palettes', default='default',
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
'sick'])
# parser.add_argument('--link_palettes', default='default',
# choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
# 'sick'])
parser.add_argument('--shield_palettes', default='default',
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
'sick'])
@@ -105,8 +104,19 @@ def main():
Alternatively, can be a ALttP Rom patched with a Link
sprite that will be extracted.
''')
parser.add_argument('--oof', help='''\
Path to a sound effect to replace Link's "oof" sound.
Needs to be in a .brr format and have a length of no
more than 2673 bytes, created from a 16-bit signed PCM
.wav at 12khz. https://github.com/boldowa/snesbrr
''')
parser.add_argument('--names', default='', type=str)
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
return parser
def main():
parser = get_argparser()
args = parser.parse_args()
args.music = not args.disablemusic
# set up logger
@@ -124,6 +134,13 @@ def main():
if args.sprite is not None and not os.path.isfile(args.sprite) and not Sprite.get_sprite_from_name(args.sprite):
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
sys.exit(1)
if args.oof is not None and not os.path.isfile(args.oof):
input('Could not find oof sound effect at given location. \nPress Enter to exit.')
sys.exit(1)
if args.oof is not None and os.path.getsize(args.oof) > 2673:
input('"oof" sound effect cannot exceed 2673 bytes. \nPress Enter to exit.')
sys.exit(1)
args, path = adjust(args=args)
if isinstance(args.sprite, Sprite):
@@ -139,7 +156,7 @@ def adjust(args):
vanillaRom = args.baserom
if not os.path.exists(vanillaRom) and not os.path.isabs(vanillaRom):
vanillaRom = local_path(vanillaRom)
if os.path.splitext(args.rom)[-1].lower() in {'.apbp', '.aplttp'}:
if os.path.splitext(args.rom)[-1].lower() == '.aplttp':
import Patch
meta, args.rom = Patch.create_rom_file(args.rom)
@@ -163,7 +180,7 @@ def adjust(args):
world = getattr(args, "world")
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.menuspeed, args.music,
args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world,
args.sprite, args.oof, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world,
deathlink=args.deathlink, allowcollect=args.allowcollect)
path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc')
rom.write_to_file(path)
@@ -178,7 +195,7 @@ def adjustGUI():
from tkinter import Tk, LEFT, BOTTOM, TOP, \
StringVar, Frame, Label, X, Entry, Button, filedialog, messagebox, ttk
from argparse import Namespace
from Main import __version__ as MWVersion
from Utils import __version__ as MWVersion
adjustWindow = Tk()
adjustWindow.wm_title("Archipelago %s LttP Adjuster" % MWVersion)
set_icon(adjustWindow)
@@ -195,7 +212,7 @@ def adjustGUI():
romEntry2 = Entry(romDialogFrame, textvariable=romVar2)
def RomSelect2():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc", ".apbp")), ("All Files", "*")])
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc", ".aplttp")), ("All Files", "*")])
romVar2.set(rom)
romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2)
@@ -225,6 +242,7 @@ def adjustGUI():
guiargs.sprite = rom_vars.sprite
if rom_vars.sprite_pool:
guiargs.world = AdjusterWorld(rom_vars.sprite_pool)
guiargs.oof = rom_vars.oof
try:
guiargs, path = adjust(args=guiargs)
@@ -263,6 +281,7 @@ def adjustGUI():
else:
guiargs.sprite = rom_vars.sprite
guiargs.sprite_pool = rom_vars.sprite_pool
guiargs.oof = rom_vars.oof
persistent_store("adjuster", GAME_ALTTP, guiargs)
messagebox.showinfo(title="Success", message="Settings saved to persistent storage")
@@ -479,6 +498,36 @@ class BackgroundTaskProgressNullWindow(BackgroundTask):
self.stop()
class AttachTooltip(object):
def __init__(self, parent, text):
self._parent = parent
self._text = text
self._window = None
parent.bind('<Enter>', lambda event : self.show())
parent.bind('<Leave>', lambda event : self.hide())
def show(self):
if self._window or not self._text:
return
self._window = Toplevel(self._parent)
#remove window bar controls
self._window.wm_overrideredirect(1)
#adjust positioning
x, y, *_ = self._parent.bbox("insert")
x = x + self._parent.winfo_rootx() + 20
y = y + self._parent.winfo_rooty() + 20
self._window.wm_geometry("+{0}+{1}".format(x,y))
#show text
label = Label(self._window, text=self._text, justify=LEFT)
label.pack(ipadx=1)
def hide(self):
if self._window:
self._window.destroy()
self._window = None
def get_rom_frame(parent=None):
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
if not adjuster_settings:
@@ -520,6 +569,7 @@ def get_rom_options_frame(parent=None):
"reduceflashing": True,
"deathlink": False,
"sprite": None,
"oof": None,
"quickswap": True,
"menuspeed": 'normal',
"heartcolor": 'red',
@@ -596,12 +646,50 @@ def get_rom_options_frame(parent=None):
spriteEntry.pack(side=LEFT)
spriteSelectButton.pack(side=LEFT)
oofDialogFrame = Frame(romOptionsFrame)
oofDialogFrame.grid(row=1, column=1)
baseOofLabel = Label(oofDialogFrame, text='"OOF" Sound:')
vars.oofNameVar = StringVar()
vars.oof = adjuster_settings.oof
def set_oof(oof_param):
nonlocal vars
if isinstance(oof_param, str) and os.path.isfile(oof_param) and os.path.getsize(oof_param) <= 2673:
vars.oof = oof_param
vars.oofNameVar.set(oof_param.rsplit('/',1)[-1])
else:
vars.oof = None
vars.oofNameVar.set('(unchanged)')
set_oof(adjuster_settings.oof)
oofEntry = Label(oofDialogFrame, textvariable=vars.oofNameVar)
def OofSelect():
nonlocal vars
oof_file = filedialog.askopenfilename(
filetypes=[("BRR files", ".brr"),
("All Files", "*")])
try:
set_oof(oof_file)
except Exception:
set_oof(None)
oofSelectButton = Button(oofDialogFrame, text='...', command=OofSelect)
AttachTooltip(oofSelectButton,
text="Select a .brr file no more than 2673 bytes.\n" + \
"This can be created from a <=0.394s 16-bit signed PCM .wav file at 12khz using snesbrr.")
baseOofLabel.pack(side=LEFT)
oofEntry.pack(side=LEFT)
oofSelectButton.pack(side=LEFT)
vars.quickSwapVar = IntVar(value=adjuster_settings.quickswap)
quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=vars.quickSwapVar)
quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
menuspeedFrame = Frame(romOptionsFrame)
menuspeedFrame.grid(row=1, column=1, sticky=E)
menuspeedFrame.grid(row=6, column=1, sticky=E)
menuspeedLabel = Label(menuspeedFrame, text='Menu speed')
menuspeedLabel.pack(side=LEFT)
vars.menuspeedVar = StringVar()
@@ -725,7 +813,7 @@ def get_rom_options_frame(parent=None):
vars.auto_apply = StringVar(value=adjuster_settings.auto_apply)
autoApplyFrame = Frame(romOptionsFrame)
autoApplyFrame.grid(row=9, column=0, columnspan=2, sticky=W)
filler = Label(autoApplyFrame, text="Automatically apply last used settings on opening .apbp files")
filler = Label(autoApplyFrame, text="Automatically apply last used settings on opening .aplttp files")
filler.pack(side=TOP, expand=True, fill=X)
askRadio = Radiobutton(autoApplyFrame, text='Ask', variable=vars.auto_apply, value='ask')
askRadio.pack(side=LEFT, padx=5, pady=5)
@@ -1054,7 +1142,6 @@ class SpriteSelector():
def custom_sprite_dir(self):
return user_path("data", "sprites", "custom")
def get_image_for_sprite(sprite, gif_only: bool = False):
if not sprite.valid:
return None

372
MMBN3Client.py Normal file
View File

@@ -0,0 +1,372 @@
import asyncio
import hashlib
import json
import os
import multiprocessing
import subprocess
import zipfile
from asyncio import StreamReader, StreamWriter
import bsdiff4
from CommonClient import CommonContext, server_loop, gui_enabled, \
ClientCommandProcessor, logger, get_base_parser
import Utils
from NetUtils import ClientStatus
from worlds.mmbn3.Items import items_by_id
from worlds.mmbn3.Rom import get_base_rom_path
from worlds.mmbn3.Locations import all_locations, scoutable_locations
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart connector_mmbn3.lua"
CONNECTION_REFUSED_STATUS = \
"Connection refused. Please start your emulator and make sure connector_mmbn3.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart connector_mmbn3.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
CONNECTION_INCORRECT_ROM = "Supplied Base Rom does not match US GBA Blue Version. Please provide the correct ROM version"
script_version: int = 2
debugEnabled = False
locations_checked = []
items_sent = []
itemIndex = 1
CHECKSUM_BLUE = "6fe31df0144759b34ad666badaacc442"
class MMBN3CommandProcessor(ClientCommandProcessor):
def __init__(self, ctx):
super().__init__(ctx)
def _cmd_gba(self):
"""Check GBA Connection State"""
if isinstance(self.ctx, MMBN3Context):
logger.info(f"GBA Status: {self.ctx.gba_status}")
def _cmd_debug(self):
"""Toggle the Debug Text overlay in ROM"""
global debugEnabled
debugEnabled = not debugEnabled
logger.info("Debug Overlay Enabled" if debugEnabled else "Debug Overlay Disabled")
class MMBN3Context(CommonContext):
command_processor = MMBN3CommandProcessor
game = "MegaMan Battle Network 3"
items_handling = 0b001 # full local
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.gba_streams: (StreamReader, StreamWriter) = None
self.gba_sync_task = None
self.gba_status = CONNECTION_INITIAL_STATUS
self.awaiting_rom = False
self.location_table = {}
self.version_warning = False
self.auth_name = None
self.slot_data = dict()
self.patching_error = False
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(MMBN3Context, self).server_auth(password_requested)
if self.auth_name is None:
self.awaiting_rom = True
logger.info("No ROM detected, awaiting conection to Bizhawk to authenticate to the multiworld server")
return
logger.info("Attempting to decode from ROM... ")
self.awaiting_rom = False
self.auth = self.auth_name.decode("utf8").replace('\x00', '')
logger.info("Connecting as "+self.auth)
await self.send_connect(name=self.auth)
def run_gui(self):
from kvui import GameManager
class MMBN3Manager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago MegaMan Battle Network 3 Client"
self.ui = MMBN3Manager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
self.slot_data = args.get("slot_data", {})
print(self.slot_data)
class ItemInfo:
id = 0x00
sender = ""
type = ""
count = 1
itemName = "Unknown"
itemID = 0x00 # Item ID, Chip ID, etc.
subItemID = 0x00 # Code for chips, color for programs
itemIndex = 1
def __init__(self, id, sender, type):
self.id = id
self.sender = sender
self.type = type
def get_json(self):
json_data = {
"id": self.id,
"sender": self.sender,
"type": self.type,
"itemName": self.itemName,
"itemID": self.itemID,
"subItemID": self.subItemID,
"count": self.count,
"itemIndex": self.itemIndex
}
return json_data
def get_payload(ctx: MMBN3Context):
global debugEnabled
items_sent = []
for i, item in enumerate(ctx.items_received):
item_data = items_by_id[item.item]
new_item = ItemInfo(i, ctx.player_names[item.player], item_data.type)
new_item.itemIndex = i+1
new_item.itemName = item_data.itemName
new_item.type = item_data.type
new_item.itemID = item_data.itemID
new_item.subItemID = item_data.subItemID
new_item.count = item_data.count
items_sent.append(new_item)
return json.dumps({
"items": [item.get_json() for item in items_sent],
"debug": debugEnabled
})
async def parse_payload(payload: dict, ctx: MMBN3Context, force: bool):
# Game completion handling
if payload["gameComplete"] and not ctx.finished_game:
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": ClientStatus.CLIENT_GOAL
}])
ctx.finished_game = True
# Locations handling
if ctx.location_table != payload["locations"]:
ctx.location_table = payload["locations"]
locs = [loc.id for loc in all_locations
if check_location_packet(loc, ctx.location_table)]
await ctx.send_msgs([{
"cmd": "LocationChecks",
"locations": locs
}])
# If trade hinting is enabled, send scout checks
if ctx.slot_data.get("trade_quest_hinting", 0) == 2:
scouted_locs = [loc.id for loc in scoutable_locations
if check_location_scouted(loc, payload["locations"])]
await ctx.send_msgs([{
"cmd": "LocationScouts",
"locations": scouted_locs,
"create_as_hint": 2
}])
def check_location_packet(location, memory):
if len(memory) == 0:
return False
# Our keys have to be strings to come through the JSON lua plugin so we have to turn our memory address into a string as well
location_key = hex(location.flag_byte)[2:]
byte = memory.get(location_key)
if byte is not None:
return byte & location.flag_mask
def check_location_scouted(location, memory):
if len(memory) == 0:
return False
location_key = hex(location.hint_flag)[2:]
byte = memory.get(location_key)
if byte is not None:
return byte & location.hint_flag_mask
async def gba_sync_task(ctx: MMBN3Context):
logger.info("Starting GBA connector. Use /gba for status information.")
if ctx.patching_error:
logger.error('Unable to Patch ROM. No ROM provided or ROM does not match US GBA Blue Version.')
while not ctx.exit_event.is_set():
error_status = None
if ctx.gba_streams:
(reader, writer) = ctx.gba_streams
msg = get_payload(ctx).encode()
writer.write(msg)
writer.write(b'\n')
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
# Data will return a dict with up to four fields
# 1. str: player name (always)
# 2. int: script version (always)
# 3. dict[str, byte]: value of location's memory byte
# 4. bool: whether the game currently registers as complete
data = await asyncio.wait_for(reader.readline(), timeout=10)
data_decoded = json.loads(data.decode())
reported_version = data_decoded.get("scriptVersion", 0)
if reported_version >= script_version:
if ctx.game is not None and "locations" in data_decoded:
# Not just a keep alive ping, parse
asyncio.create_task((parse_payload(data_decoded, ctx, False)))
if not ctx.auth:
ctx.auth_name = bytes(data_decoded["playerName"])
if ctx.awaiting_rom:
logger.info("Awaiting data from ROM...")
await ctx.server_auth(False)
else:
if not ctx.version_warning:
logger.warning(f"Your Lua script is version {reported_version}, expected {script_version}."
"Please update to the latest version."
"Your connection to the Archipelago server will not be accepted.")
ctx.version_warning = True
except asyncio.TimeoutError:
logger.debug("Read Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.gba_streams = None
except ConnectionResetError:
logger.debug("Read failed due to Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.gba_streams = None
except TimeoutError:
logger.debug("Connection Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.gba_streams = None
except ConnectionResetError:
logger.debug("Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.gba_streams = None
if ctx.gba_status == CONNECTION_TENTATIVE_STATUS:
if not error_status:
logger.info("Successfully Connected to GBA")
ctx.gba_status = CONNECTION_CONNECTED_STATUS
else:
ctx.gba_status = f"Was tentatively connected but error occurred: {error_status}"
elif error_status:
ctx.gba_status = error_status
logger.info("Lost connection to GBA and attempting to reconnect. Use /gba for status updates")
else:
try:
logger.debug("Attempting to connect to GBA")
ctx.gba_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 28922), timeout=10)
ctx.gba_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
ctx.gba_status = CONNECTION_TIMING_OUT_STATUS
continue
except ConnectionRefusedError:
logger.debug("Connection Refused, Trying Again")
ctx.gba_status = CONNECTION_REFUSED_STATUS
continue
async def run_game(romfile):
options = Utils.get_options().get("mmbn3_options", None)
if options is None:
auto_start = True
else:
auto_start = options.get("rom_start", True)
if auto_start:
import webbrowser
webbrowser.open(romfile)
elif os.path.isfile(auto_start):
subprocess.Popen([auto_start, romfile],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
async def patch_and_run_game(apmmbn3_file):
base_name = os.path.splitext(apmmbn3_file)[0]
with zipfile.ZipFile(apmmbn3_file, 'r') as patch_archive:
try:
with patch_archive.open("delta.bsdiff4", 'r') as stream:
patch_data = stream.read()
except KeyError:
raise FileNotFoundError("Patch file missing from archive.")
rom_file = get_base_rom_path()
with open(rom_file, 'rb') as rom:
rom_bytes = rom.read()
patched_bytes = bsdiff4.patch(rom_bytes, patch_data)
patched_rom_file = base_name+".gba"
with open(patched_rom_file, 'wb') as patched_rom:
patched_rom.write(patched_bytes)
asyncio.create_task(run_game(patched_rom_file))
def confirm_checksum():
rom_file = get_base_rom_path()
if not os.path.exists(rom_file):
return False
with open(rom_file, 'rb') as rom:
rom_bytes = rom.read()
basemd5 = hashlib.md5()
basemd5.update(rom_bytes)
return CHECKSUM_BLUE == basemd5.hexdigest()
if __name__ == "__main__":
Utils.init_logging("MMBN3Client")
async def main():
multiprocessing.freeze_support()
parser = get_base_parser()
parser.add_argument("patch_file", default="", type=str, nargs="?",
help="Path to an APMMBN3 file")
args = parser.parse_args()
checksum_matches = confirm_checksum()
if checksum_matches:
if args.patch_file:
asyncio.create_task(patch_and_run_game(args.patch_file))
ctx = MMBN3Context(args.connect, args.password)
if not checksum_matches:
ctx.patching_error = True
ctx.server_task = asyncio.create_task(server_loop(ctx), name="Server Loop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
ctx.gba_sync_task = asyncio.create_task(gba_sync_task(ctx), name="GBA Sync")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.gba_sync_task:
await ctx.gba_sync_task
import colorama
colorama.init()
asyncio.run(main())
colorama.deinit()

341
Main.py
View File

@@ -1,23 +1,26 @@
import collections
from itertools import zip_longest, chain
import concurrent.futures
import logging
import os
import time
import zlib
import concurrent.futures
import pickle
import tempfile
import time
import zipfile
from typing import Dict, Tuple, Optional, Set
import zlib
from typing import Dict, List, Optional, Set, Tuple
from BaseClasses import MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location
from worlds.alttp.Items import item_name_groups
from worlds.alttp.Regions import is_main_entrance
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
from worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
from Utils import output_path, get_options, __version__, version_tuple
from worlds.generic.Rules import locality_rules, exclusion_rules, group_locality_rules
import worlds
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region
from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items
from Options import StartInventoryPool
from Utils import __version__, get_options, output_path, version_tuple
from worlds import AutoWorld
from worlds.alttp.Regions import is_main_entrance
from worlds.alttp.Shops import FillDisabledShopSlots
from worlds.alttp.SubClasses import LTTPRegionType
from worlds.generic.Rules import exclusion_rules, locality_rules
__all__ = ["main"]
ordered_areas = (
'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
@@ -38,7 +41,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world = MultiWorld(args.multi)
logger = logging.getLogger()
world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed))
world.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
world.plando_options = args.plando_options
world.shuffle = args.shuffle.copy()
world.logic = args.logic.copy()
@@ -52,7 +56,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world.enemy_damage = args.enemy_damage.copy()
world.beemizer_total_chance = args.beemizer_total_chance.copy()
world.beemizer_trap_chance = args.beemizer_trap_chance.copy()
world.timer = args.timer.copy()
world.countdown_start_time = args.countdown_start_time.copy()
world.red_clock_time = args.red_clock_time.copy()
world.blue_clock_time = args.blue_clock_time.copy()
@@ -78,17 +81,32 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world.state = CollectionState(world)
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
logger.info("Found World Types:")
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
numlength = 8
max_item = 0
max_location = 0
for cls in AutoWorld.AutoWorldRegister.world_types.values():
if cls.item_id_to_name:
max_item = max(max_item, max(cls.item_id_to_name))
max_location = max(max_location, max(cls.location_id_to_name))
item_digits = len(str(max_item))
location_digits = len(str(max_location))
item_count = len(str(max(len(cls.item_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
location_count = len(str(max(len(cls.location_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
del max_item, max_location
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
if not cls.hidden:
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} "
f"Items (IDs: {min(cls.item_id_to_name):{numlength}} - "
f"{max(cls.item_id_to_name):{numlength}}) | "
f"{len(cls.location_names):3} "
f"Locations (IDs: {min(cls.location_id_to_name):{numlength}} - "
f"{max(cls.location_id_to_name):{numlength}})")
if not cls.hidden and len(cls.item_names) > 0:
logger.info(f" {name:{longest_name}}: {len(cls.item_names):{item_count}} "
f"Items (IDs: {min(cls.item_id_to_name):{item_digits}} - "
f"{max(cls.item_id_to_name):{item_digits}}) | "
f"{len(cls.location_names):{location_count}} "
f"Locations (IDs: {min(cls.location_id_to_name):{location_digits}} - "
f"{max(cls.location_id_to_name):{location_digits}})")
del item_digits, location_digits, item_count, location_count
AutoWorld.call_stage(world, "assert_generate")
@@ -101,18 +119,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for _ in range(count):
world.push_precollected(world.create_item(item_name, player))
for player in world.player_ids:
if player in world.get_game_players("A Link to the Past"):
# enforce pre-defined local items.
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
world.local_items[player].value.add('Triforce Piece')
# Not possible to place pendants/crystals out side of boss prizes yet.
world.non_local_items[player].value -= item_name_groups['Pendants']
world.non_local_items[player].value -= item_name_groups['Crystals']
# items can't be both local and non-local, prefer local
world.non_local_items[player].value -= world.local_items[player].value
for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items():
for _ in range(count):
world.push_precollected(world.create_item(item_name, player))
logger.info('Creating World.')
AutoWorld.call_all(world, "create_regions")
@@ -120,11 +129,19 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.info('Creating Items.')
AutoWorld.call_all(world, "create_items")
# All worlds should have finished creating all regions, locations, and entrances.
# Recache to ensure that they are all visible for locality rules.
world._recache()
logger.info('Calculating Access Rules.')
for player in world.player_ids:
# items can't be both local and non-local, prefer local
world.non_local_items[player].value -= world.local_items[player].value
world.non_local_items[player].value -= set(world.local_early_items[player])
if world.players > 1:
for player in world.player_ids:
locality_rules(world, player)
group_locality_rules(world)
locality_rules(world)
else:
world.non_local_items[1].value = set()
world.local_items[1].value = set()
@@ -139,10 +156,43 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
AutoWorld.call_all(world, "generate_basic")
# remove starting inventory from pool items.
# Because some worlds don't actually create items during create_items this has to be as late as possible.
if any(world.start_inventory_from_pool[player].value for player in world.player_ids):
new_items: List[Item] = []
depletion_pool: Dict[int, Dict[str, int]] = {
player: world.start_inventory_from_pool[player].value.copy() for player in world.player_ids}
for player, items in depletion_pool.items():
player_world: AutoWorld.World = world.worlds[player]
for count in items.values():
new_items.append(player_world.create_filler())
target: int = sum(sum(items.values()) for items in depletion_pool.values())
for i, item in enumerate(world.itempool):
if depletion_pool[item.player].get(item.name, 0):
target -= 1
depletion_pool[item.player][item.name] -= 1
# quick abort if we have found all items
if not target:
new_items.extend(world.itempool[i+1:])
break
else:
new_items.append(item)
# leftovers?
if target:
for player, remaining_items in depletion_pool.items():
remaining_items = {name: count for name, count in remaining_items.items() if count}
if remaining_items:
raise Exception(f"{world.get_player_name(player)}"
f" is trying to remove items from their pool that don't exist: {remaining_items}")
world.itempool[:] = new_items
# temporary home for item links, should be moved out of Main
for group_id, group in world.groups.items():
def find_common_pool(players: Set[int], shared_pool: Set[str]):
classifications = collections.defaultdict(int)
def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]]
]:
classifications: Dict[str, int] = collections.defaultdict(int)
counters = {player: {name: 0 for name in shared_pool} for player in players}
for item in world.itempool:
if item.player in counters and item.name in shared_pool:
@@ -152,7 +202,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for player in players.copy():
if all([counters[player][item] == 0 for item in shared_pool]):
players.remove(player)
del(counters[player])
del (counters[player])
if not players:
return None, None
@@ -164,14 +214,14 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
counters[player][item] = count
else:
for player in players:
del(counters[player][item])
del (counters[player][item])
return counters, classifications
common_item_count, classifications = find_common_pool(group["players"], group["item_pool"])
if not common_item_count:
continue
new_itempool = []
new_itempool: List[Item] = []
for item_name, item_count in next(iter(common_item_count.values())).items():
for _ in range(item_count):
new_item = group["world"].create_item(item_name)
@@ -179,7 +229,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
new_item.classification |= classifications[item_name]
new_itempool.append(new_item)
region = Region("Menu", RegionType.Generic, "ItemLink", group_id, world)
region = Region("Menu", group_id, world, "ItemLink")
world.regions.append(region)
locations = region.locations = []
for item in world.itempool:
@@ -202,11 +252,15 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
while itemcount > len(world.itempool):
items_to_add = []
for player in group["players"]:
if group["link_replacement"]:
item_player = group_id
else:
item_player = player
if group["replacement_items"][player]:
items_to_add.append(AutoWorld.call_single(world, "create_item", player,
items_to_add.append(AutoWorld.call_single(world, "create_item", item_player,
group["replacement_items"][player]))
else:
items_to_add.append(AutoWorld.call_single(world, "create_filler", player))
items_to_add.append(AutoWorld.call_single(world, "create_filler", item_player))
world.random.shuffle(items_to_add)
world.itempool.extend(items_to_add[:itemcount - len(world.itempool)])
@@ -231,10 +285,16 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
AutoWorld.call_all(world, 'post_fill')
if world.players > 1:
if world.players > 1 and not args.skip_prog_balancing:
balance_multiworld_progression(world)
else:
logger.info("Progression balancing skipped.")
logger.info(f'Beginning output...')
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
world.random.passthrough = False
outfilebase = 'AP_' + world.seed_name
output = tempfile.TemporaryDirectory()
@@ -270,37 +330,16 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
'Inverted Ganons Tower': 'Ganons Tower'} \
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player][dungeonname].append(location.address)
elif location.parent_region.type == RegionType.LightWorld:
elif location.parent_region.type == LTTPRegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.type == RegionType.DarkWorld:
elif location.parent_region.type == LTTPRegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.LightWorld:
elif main_entrance.parent_region.type == LTTPRegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.DarkWorld:
elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
checks_in_area[location.player]["Total"] += 1
oldmancaves = []
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
for index, take_any in enumerate(takeanyregions):
for region in [world.get_region(take_any, player) for player in
world.get_game_players("A Link to the Past") if world.retro_caves[player]]:
item = world.create_item(
region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
region.player)
player = region.player
location_id = SHOP_ID_START + total_shop_slots + index
main_entrance = region.get_connecting_entrance(is_main_entrance)
if main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[player]["Light World"].append(location_id)
else:
checks_in_area[player]["Dark World"].append(location_id)
checks_in_area[player]["Total"] += 1
er_hint_data[player][location_id] = main_entrance.name
oldmancaves.append(((location_id, player), (item.code, player)))
FillDisabledShopSlots(world)
def write_multidata():
@@ -356,16 +395,17 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for player in world.groups.get(location.item.player, {}).get("players", [])]):
precollect_hint(location)
# embedded data package
data_package = {
game_world.game: worlds.network_data_package["games"][game_world.game]
for game_world in world.worlds.values()
}
multidata = {
"slot_data": slot_data,
"slot_info": slot_info,
"names": names, # TODO: remove around 0.2.5 in favor of slot_info
"games": games, # TODO: remove around 0.2.5 in favor of slot_info
"names": names, # TODO: remove after 0.3.9
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
"remote_items": {player for player in world.player_ids if
world.worlds[player].remote_items},
"remote_start_inventory": {player for player in world.player_ids if
world.worlds[player].remote_start_inventory},
"locations": locations_data,
"checks_in_area": checks_in_area,
"server_options": baked_server_options,
@@ -375,7 +415,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
"version": tuple(version_tuple),
"tags": ["AP"],
"minimum_versions": minimum_versions,
"seed_name": world.seed_name
"seed_name": world.seed_name,
"datapackage": data_package,
}
AutoWorld.call_all(world, "modify_multidata", multidata)
@@ -401,7 +442,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
if args.spoiler > 1:
logger.info('Calculating playthrough.')
create_playthrough(world)
world.spoiler.create_playthrough(create_paths=args.spoiler > 2)
if args.spoiler:
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
@@ -415,143 +456,3 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
return world
def create_playthrough(world):
"""Destructive to the world while it is run, damage gets repaired afterwards."""
# get locations containing progress items
prog_locations = {location for location in world.get_filled_locations() if location.item.advancement}
state_cache = [None]
collection_spheres = []
state = CollectionState(world)
sphere_candidates = set(prog_locations)
logging.debug('Building up collection spheres.')
while sphere_candidates:
# build up spheres of collection radius.
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
sphere = {location for location in sphere_candidates if state.can_reach(location)}
for location in sphere:
state.collect(location.item, True, location)
sphere_candidates -= sphere
collection_spheres.append(sphere)
state_cache.append(state.copy())
logging.debug('Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere),
len(prog_locations))
if not sphere:
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
location.item.name, location.item.player, location.name, location.player) for location in
sphere_candidates])
if any([world.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]):
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
f'Something went terribly wrong here.')
else:
world.spoiler.unreachables = sphere_candidates
break
# in the second phase, we cull each sphere such that the game is still beatable,
# reducing each range of influence to the bare minimum required inside it
restore_later = {}
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
to_delete = set()
for location in sphere:
# we remove the item at location and check if game is still beatable
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
location.item.player)
old_item = location.item
location.item = None
if world.can_beat_game(state_cache[num]):
to_delete.add(location)
restore_later[location] = old_item
else:
# still required, got to keep it around
location.item = old_item
# cull entries in spheres for spoiler walkthrough at end
sphere -= to_delete
# second phase, sphere 0
removed_precollected = []
for item in (i for i in chain.from_iterable(world.precollected_items.values()) if i.advancement):
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
world.precollected_items[item.player].remove(item)
world.state.remove(item)
if not world.can_beat_game():
world.push_precollected(item)
else:
removed_precollected.append(item)
# we are now down to just the required progress items in collection_spheres. Unfortunately
# the previous pruning stage could potentially have made certain items dependant on others
# in the same or later sphere (because the location had 2 ways to access but the item originally
# used to access it was deemed not required.) So we need to do one final sphere collection pass
# to build up the correct spheres
required_locations = {item for sphere in collection_spheres for item in sphere}
state = CollectionState(world)
collection_spheres = []
while required_locations:
state.sweep_for_events(key_only=True)
sphere = set(filter(state.can_reach, required_locations))
for location in sphere:
state.collect(location.item, True, location)
required_locations -= sphere
collection_spheres.append(sphere)
logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres),
len(sphere), len(required_locations))
if not sphere:
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
def flist_to_iter(node):
while node:
value, node = node
yield value
def get_path(state, region):
reversed_path_as_flist = state.path.get(region, (region, None))
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
# Now we combine the flat string list into (region, exit) pairs
pathsiter = iter(string_path_flat)
pathpairs = zip_longest(pathsiter, pathsiter)
return list(pathpairs)
world.spoiler.paths = {}
topology_worlds = (player for player in world.player_ids if world.worlds[player].topology_present)
for player in topology_worlds:
world.spoiler.paths.update(
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
sphere if location.player == player})
if player in world.get_game_players("A Link to the Past"):
# If Pyramid Fairy Entrance needs to be reached, also path to Big Bomb Shop
# Maybe move the big bomb over to the Event system instead?
if any(exit_path == 'Pyramid Fairy' for path in world.spoiler.paths.values() for (_, exit_path) in path):
if world.mode[player] != 'inverted':
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \
get_path(state, world.get_region('Big Bomb Shop', player))
else:
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \
get_path(state, world.get_region('Inverted Big Bomb Shop', player))
# we can finally output our playthrough
world.spoiler.playthrough = {"0": sorted([str(item) for item in
chain.from_iterable(world.precollected_items.values())
if item.advancement])}
for i, sphere in enumerate(collection_spheres):
world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sorted(sphere)}
# repair the world again
for location, item in restore_later.items():
location.item = item
for item in removed_precollected:
world.push_precollected(item)

View File

@@ -77,49 +77,34 @@ def read_apmc_file(apmc_file):
return json.loads(b64decode(f.read()))
def update_mod(forge_dir, minecraft_version: str, get_prereleases=False):
def update_mod(forge_dir, url: str):
"""Check mod version, download new mod from GitHub releases page if needed. """
ap_randomizer = find_ap_randomizer_jar(forge_dir)
client_releases_endpoint = "https://api.github.com/repos/KonoTyran/Minecraft_AP_Randomizer/releases"
resp = requests.get(client_releases_endpoint)
if resp.status_code == 200: # OK
try:
latest_release = next(filter(lambda release: (not release['prerelease'] or get_prereleases) and
(minecraft_version in release['assets'][0]['name']),
resp.json()))
if ap_randomizer != latest_release['assets'][0]['name']:
logging.info(f"A new release of the Minecraft AP randomizer mod was found: "
f"{latest_release['assets'][0]['name']}")
if ap_randomizer is not None:
logging.info(f"Your current mod is {ap_randomizer}.")
else:
logging.info(f"You do not have the AP randomizer mod installed.")
if prompt_yes_no("Would you like to update?"):
old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None
new_ap_mod = os.path.join(forge_dir, 'mods', latest_release['assets'][0]['name'])
logging.info("Downloading AP randomizer mod. This may take a moment...")
apmod_resp = requests.get(latest_release['assets'][0]['browser_download_url'])
if apmod_resp.status_code == 200:
with open(new_ap_mod, 'wb') as f:
f.write(apmod_resp.content)
logging.info(f"Wrote new mod file to {new_ap_mod}")
if old_ap_mod is not None:
os.remove(old_ap_mod)
logging.info(f"Removed old mod file from {old_ap_mod}")
else:
logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
logging.error(f"Please report this issue on the Archipelago Discord server.")
sys.exit(1)
except StopIteration:
logging.warning(f"No compatible mod version found for {minecraft_version}.")
if not prompt_yes_no("Run server anyway?"):
sys.exit(0)
os.path.basename(url)
if ap_randomizer is not None:
logging.info(f"Your current mod is {ap_randomizer}.")
else:
logging.error(f"Error checking for randomizer mod updates (status code {resp.status_code}).")
logging.error(f"If this was not expected, please report this issue on the Archipelago Discord server.")
if not prompt_yes_no("Continue anyways?"):
sys.exit(0)
logging.info(f"You do not have the AP randomizer mod installed.")
if ap_randomizer != os.path.basename(url):
logging.info(f"A new release of the Minecraft AP randomizer mod was found: "
f"{os.path.basename(url)}")
if prompt_yes_no("Would you like to update?"):
old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None
new_ap_mod = os.path.join(forge_dir, 'mods', os.path.basename(url))
logging.info("Downloading AP randomizer mod. This may take a moment...")
apmod_resp = requests.get(url)
if apmod_resp.status_code == 200:
with open(new_ap_mod, 'wb') as f:
f.write(apmod_resp.content)
logging.info(f"Wrote new mod file to {new_ap_mod}")
if old_ap_mod is not None:
os.remove(old_ap_mod)
logging.info(f"Removed old mod file from {old_ap_mod}")
else:
logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
logging.error(f"Please report this issue on the Archipelago Discord server.")
sys.exit(1)
def check_eula(forge_dir):
@@ -264,8 +249,13 @@ def get_minecraft_versions(version, release_channel="release"):
return next(filter(lambda entry: entry["version"] == version, data[release_channel]))
else:
return resp.json()[release_channel][0]
except StopIteration:
logging.error(f"No compatible mod version found for client version {version}.")
except (StopIteration, KeyError):
logging.error(f"No compatible mod version found for client version {version} on \"{release_channel}\" channel.")
if release_channel != "release":
logging.error("Consider switching \"release_channel\" to \"release\" in your Host.yaml file")
else:
logging.error("No suitable mod found on the \"release\" channel. Please Contact us on discord to report this error.")
sys.exit(0)
def is_correct_forge(forge_dir) -> bool:
@@ -286,6 +276,8 @@ if __name__ == '__main__':
help="specify java version.")
parser.add_argument('--forge', '-f', metavar='1.18.2-40.1.0', dest='forge', type=str, default=False, action='store',
help="specify forge version. (Minecraft Version-Forge Version)")
parser.add_argument('--version', '-v', metavar='9', dest='data_version', type=int, action='store',
help="specify Mod data version to download.")
args = parser.parse_args()
apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None
@@ -296,12 +288,12 @@ if __name__ == '__main__':
options = Utils.get_options()
channel = args.channel or options["minecraft_options"]["release_channel"]
apmc_data = None
data_version = None
data_version = args.data_version or None
if apmc_file is None and not args.install:
apmc_file = Utils.open_filename('Select APMC file', (('APMC File', ('.apmc',)),))
if apmc_file is not None:
if apmc_file is not None and data_version is None:
apmc_data = read_apmc_file(apmc_file)
data_version = apmc_data.get('client_version', '')
@@ -311,6 +303,7 @@ if __name__ == '__main__':
max_heap = options["minecraft_options"]["max_heap_size"]
forge_version = args.forge or versions["forge"]
java_version = args.java or versions["java"]
mod_url = versions["url"]
java_dir = find_jdk_dir(java_version)
if args.install:
@@ -344,7 +337,7 @@ if __name__ == '__main__':
if not max_heap_re.match(max_heap):
raise Exception(f"Max heap size {max_heap} in incorrect format. Use a number followed by M or G, e.g. 512M or 2G.")
update_mod(forge_dir, f"MC{forge_version.split('-')[0]}", channel != "release")
update_mod(forge_dir, mod_url)
replace_apmc_files(forge_dir, apmc_file)
check_eula(forge_dir)
server_process = run_forge_server(forge_dir, java_version, max_heap)

View File

@@ -1,7 +1,8 @@
import os
import sys
import subprocess
import pkg_resources
import multiprocessing
import warnings
local_dir = os.path.dirname(__file__)
requirements_files = {os.path.join(local_dir, 'requirements.txt')}
@@ -9,49 +10,109 @@ requirements_files = {os.path.join(local_dir, 'requirements.txt')}
if sys.version_info < (3, 8, 6):
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
update_ran = getattr(sys, "frozen", False) # don't run update if environment is frozen/compiled
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
update_ran = getattr(sys, "frozen", False) or multiprocessing.parent_process()
if not update_ran:
for entry in os.scandir(os.path.join(local_dir, "worlds")):
if entry.is_dir():
req_file = os.path.join(entry.path, "requirements.txt")
if os.path.exists(req_file):
requirements_files.add(req_file)
# skip .* (hidden / disabled) folders
if not entry.name.startswith("."):
if entry.is_dir():
req_file = os.path.join(entry.path, "requirements.txt")
if os.path.exists(req_file):
requirements_files.add(req_file)
def check_pip():
# detect if pip is available
try:
import pip # noqa: F401
except ImportError:
raise RuntimeError("pip not available. Please install pip.")
def confirm(msg: str):
try:
input(f"\n{msg}")
except KeyboardInterrupt:
print("\nAborting")
sys.exit(1)
def update_command():
check_pip()
for file in requirements_files:
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade'])
subprocess.call([sys.executable, "-m", "pip", "install", "-r", file, "--upgrade"])
def install_pkg_resources(yes=False):
try:
import pkg_resources # noqa: F401
except ImportError:
check_pip()
if not yes:
confirm("pkg_resources not found, press enter to install it")
subprocess.call([sys.executable, "-m", "pip", "install", "--upgrade", "setuptools"])
def update(yes=False, force=False):
global update_ran
if not update_ran:
update_ran = True
if force:
update_command()
return
install_pkg_resources(yes=yes)
import pkg_resources
for req_file in requirements_files:
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
if not os.path.exists(path):
path = os.path.join(os.path.dirname(__file__), req_file)
with open(path) as requirementsfile:
for line in requirementsfile:
if line.startswith('https://'):
# extract name and version from url
wheel = line.split('/')[-1]
name, version, _ = wheel.split('-', 2)
line = f'{name}=={version}'
if not line or line[0] == "#":
continue # ignore comments
if line.startswith(("https://", "git+https://")):
# extract name and version for url
rest = line.split('/')[-1]
line = ""
if "#egg=" in rest:
# from egg info
rest, egg = rest.split("#egg=", 1)
egg = egg.split(";", 1)[0].rstrip()
if any(compare in egg for compare in ("==", ">=", ">", "<", "<=", "!=")):
warnings.warn(f"Specifying version as #egg={egg} will become unavailable in pip 25.0. "
"Use name @ url#version instead.", DeprecationWarning)
line = egg
else:
egg = ""
if "@" in rest and not line:
raise ValueError("Can't deduce version from requirement")
elif not line:
# from filename
rest = rest.replace(".zip", "-").replace(".tar.gz", "-")
name, version, _ = rest.split("-", 2)
line = f'{egg or name}=={version}'
elif "@" in line and "#" in line:
# PEP 508 does not allow us to specify a version, so we use custom syntax
# name @ url#version ; marker
name, rest = line.split("@", 1)
version = rest.split("#", 1)[1].split(";", 1)[0].rstrip()
line = f"{name.rstrip()}=={version}"
if ";" in rest: # keep marker
line += rest[rest.find(";"):]
requirements = pkg_resources.parse_requirements(line)
for requirement in requirements:
requirement = str(requirement)
for requirement in map(str, requirements):
try:
pkg_resources.require(requirement)
except pkg_resources.ResolutionError:
if not yes:
import traceback
traceback.print_exc()
input(f'Requirement {requirement} is not satisfied, press enter to install it')
confirm(f"Requirement {requirement} is not satisfied, press enter to install it")
update_command()
return

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ from json import JSONEncoder, JSONDecoder
import websockets
from Utils import Version
from Utils import ByValue, Version
class JSONMessagePart(typing.TypedDict, total=False):
@@ -20,7 +20,7 @@ class JSONMessagePart(typing.TypedDict, total=False):
flags: int
class ClientStatus(enum.IntEnum):
class ClientStatus(ByValue, enum.IntEnum):
CLIENT_UNKNOWN = 0
CLIENT_CONNECTED = 5
CLIENT_READY = 10
@@ -28,22 +28,22 @@ class ClientStatus(enum.IntEnum):
CLIENT_GOAL = 30
class SlotType(enum.IntFlag):
class SlotType(ByValue, enum.IntFlag):
spectator = 0b00
player = 0b01
group = 0b10
@property
def always_goal(self) -> bool:
"""Mark this slot has having reached its goal instantly."""
"""Mark this slot as having reached its goal instantly."""
return self.value != 0b01
class Permission(enum.IntFlag):
class Permission(ByValue, enum.IntFlag):
disabled = 0b000 # 0, completely disables access
enabled = 0b001 # 1, allows manual use
goal = 0b010 # 2, allows manual use after goal completion
auto = 0b110 # 6, forces use after goal completion, only works for forfeit
auto = 0b110 # 6, forces use after goal completion, only works for release
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
@staticmethod
@@ -86,7 +86,7 @@ def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any:
data = obj._asdict()
data["class"] = obj.__class__.__name__
return data
if isinstance(obj, (tuple, list, set)):
if isinstance(obj, (tuple, list, set, frozenset)):
return tuple(_scan_for_TypedTuples(o) for o in obj)
if isinstance(obj, dict):
return {key: _scan_for_TypedTuples(value) for key, value in obj.items()}
@@ -100,7 +100,7 @@ _encode = JSONEncoder(
).encode
def encode(obj):
def encode(obj: typing.Any) -> str:
return _encode(_scan_for_TypedTuples(obj))
@@ -109,7 +109,7 @@ def get_any_version(data: dict) -> Version:
return Version(int(data["major"]), int(data["minor"]), int(data["build"]))
whitelist = {
allowlist = {
"NetworkPlayer": NetworkPlayer,
"NetworkItem": NetworkItem,
"NetworkSlot": NetworkSlot
@@ -125,7 +125,7 @@ def _object_hook(o: typing.Any) -> typing.Any:
hook = custom_hooks.get(o.get("class", None), None)
if hook:
return hook(o)
cls = whitelist.get(o.get("class", None), None)
cls = allowlist.get(o.get("class", None), None)
if cls:
for key in tuple(o):
if key not in cls._fields:

View File

@@ -3,6 +3,7 @@ import argparse
import logging
import random
import os
import zipfile
from itertools import chain
from BaseClasses import MultiWorld
@@ -43,7 +44,7 @@ def adjustGUI():
StringVar, IntVar, Checkbutton, Frame, Label, X, Entry, Button, \
OptionMenu, filedialog, messagebox, ttk
from argparse import Namespace
from Main import __version__ as MWVersion
from Utils import __version__ as MWVersion
window = tk.Tk()
window.wm_title(f"Archipelago {MWVersion} OoT Adjuster")
@@ -196,7 +197,7 @@ def set_icon(window):
def adjust(args):
# Create a fake world and OOTWorld to use as a base
world = MultiWorld(1)
world.slot_seeds = {1: random}
world.per_slot_randoms = {1: random}
ootworld = OOTWorld(world, 1)
# Set options in the fake OOTWorld
for name, option in chain(cosmetic_options.items(), sfx_options.items()):
@@ -217,13 +218,18 @@ def adjust(args):
# Load up the ROM
rom = Rom(file=args.rom, force_use=True)
delete_zootdec = True
elif os.path.splitext(args.rom)[-1] == '.apz5':
elif os.path.splitext(args.rom)[-1] in ['.apz5', '.zpf']:
# Load vanilla ROM
rom = Rom(file=args.vanilla_rom, force_use=True)
apz5_file = args.rom
base_name = os.path.splitext(apz5_file)[0]
# Patch file
apply_patch_file(rom, args.rom)
apply_patch_file(rom, apz5_file,
sub_file=(os.path.basename(base_name) + '.zpf'
if zipfile.is_zipfile(apz5_file)
else None))
else:
raise Exception("Invalid file extension; requires .n64, .z64, .apz5")
raise Exception("Invalid file extension; requires .n64, .z64, .apz5, .zpf")
# Call patch_cosmetics
try:
patch_cosmetics(ootworld, rom)

View File

@@ -3,20 +3,23 @@ import json
import os
import multiprocessing
import subprocess
import zipfile
from asyncio import StreamReader, StreamWriter
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, \
# CommonClient import first to trigger ModuleUpdater
from CommonClient import CommonContext, server_loop, gui_enabled, \
ClientCommandProcessor, logger, get_base_parser
import Utils
from Utils import async_start
from worlds import network_data_package
from worlds.oot.Rom import Rom, compress_rom_file
from worlds.oot.N64Patch import apply_patch_file
from worlds.oot.Utils import data_path
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart oot_connector.lua"
CONNECTION_REFUSED_STATUS = "Connection refused. Please start your emulator and make sure oot_connector.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart oot_connector.lua"
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart connector_oot.lua"
CONNECTION_REFUSED_STATUS = "Connection refused. Please start your emulator and make sure connector_oot.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart connector_oot.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
@@ -48,7 +51,7 @@ deathlink_sent_this_death: we interacted with the multiworld on this death, wait
oot_loc_name_to_id = network_data_package["games"]["Ocarina of Time"]["location_name_to_id"]
script_version: int = 2
script_version: int = 3
def get_item_value(ap_id):
return ap_id - 66000
@@ -68,7 +71,7 @@ class OoTCommandProcessor(ClientCommandProcessor):
if isinstance(self.ctx, OoTContext):
self.ctx.deathlink_client_override = True
self.ctx.deathlink_enabled = not self.ctx.deathlink_enabled
asyncio.create_task(self.ctx.update_death_link(self.ctx.deathlink_enabled), name="Update Deathlink")
async_start(self.ctx.update_death_link(self.ctx.deathlink_enabled), name="Update Deathlink")
class OoTContext(CommonContext):
@@ -83,6 +86,9 @@ class OoTContext(CommonContext):
self.n64_status = CONNECTION_INITIAL_STATUS
self.awaiting_rom = False
self.location_table = {}
self.collectible_table = {}
self.collectible_override_flags_address = 0
self.collectible_offsets = {}
self.deathlink_enabled = False
self.deathlink_pending = False
self.deathlink_sent_this_death = False
@@ -94,7 +100,7 @@ class OoTContext(CommonContext):
await super(OoTContext, self).server_auth(password_requested)
if not self.auth:
self.awaiting_rom = True
logger.info('Awaiting connection to Bizhawk to get player information')
logger.info('Awaiting connection to EmuHawk to get player information')
return
await self.send_connect()
@@ -115,6 +121,13 @@ class OoTContext(CommonContext):
self.ui = OoTManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def on_package(self, cmd, args):
if cmd == 'Connected':
slot_data = args.get('slot_data', None)
if slot_data:
self.collectible_override_flags_address = slot_data.get('collectible_override_flags', 0)
self.collectible_offsets = slot_data.get('collectible_flag_offsets', {})
def get_payload(ctx: OoTContext):
if ctx.deathlink_enabled and ctx.deathlink_pending:
@@ -123,15 +136,32 @@ def get_payload(ctx: OoTContext):
else:
trigger_death = False
return json.dumps({
payload = json.dumps({
"items": [get_item_value(item.item) for item in ctx.items_received],
"playerNames": [name for (i, name) in ctx.player_names.items() if i != 0],
"triggerDeath": trigger_death
"triggerDeath": trigger_death,
"collectibleOverrides": ctx.collectible_override_flags_address,
"collectibleOffsets": ctx.collectible_offsets
})
return payload
async def parse_payload(payload: dict, ctx: OoTContext, force: bool):
# Refuse to do anything if ROM is detected as changed
if ctx.auth and payload['playerName'] != ctx.auth:
logger.warning("ROM change detected. Disconnecting and reconnecting...")
ctx.deathlink_enabled = False
ctx.deathlink_client_override = False
ctx.finished_game = False
ctx.location_table = {}
ctx.collectible_table = {}
ctx.deathlink_pending = False
ctx.deathlink_sent_this_death = False
ctx.auth = payload['playerName']
await ctx.send_connect()
return
# Turn on deathlink if it is on, and if the client hasn't overriden it
if payload['deathlinkActive'] and not ctx.deathlink_enabled and not ctx.deathlink_client_override:
await ctx.update_death_link(True)
@@ -146,11 +176,23 @@ async def parse_payload(payload: dict, ctx: OoTContext, force: bool):
ctx.finished_game = True
# Locations handling
if ctx.location_table != payload['locations']:
ctx.location_table = payload['locations']
locations = payload['locations']
collectibles = payload['collectibles']
# The Lua JSON library serializes an empty table into a list instead of a dict. Verify types for safety:
if isinstance(locations, list):
locations = {}
if isinstance(collectibles, list):
collectibles = {}
if ctx.location_table != locations or ctx.collectible_table != collectibles:
ctx.location_table = locations
ctx.collectible_table = collectibles
locs1 = [oot_loc_name_to_id[loc] for loc, b in ctx.location_table.items() if b]
locs2 = [int(loc) for loc, b in ctx.collectible_table.items() if b]
await ctx.send_msgs([{
"cmd": "LocationChecks",
"locations": [oot_loc_name_to_id[loc] for loc in ctx.location_table if ctx.location_table[loc]]
"locations": locs1 + locs2
}])
# Deathlink handling
@@ -176,20 +218,13 @@ async def n64_sync_task(ctx: OoTContext):
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
# Data will return a dict with up to six fields:
# 1. str: player name (always)
# 2. int: script version (always)
# 3. bool: deathlink active (always)
# 4. dict[str, bool]: checked locations
# 5. bool: whether Link is currently at 0 HP
# 6. bool: whether the game currently registers as complete
data = await asyncio.wait_for(reader.readline(), timeout=10)
data_decoded = json.loads(data.decode())
reported_version = data_decoded.get('scriptVersion', 0)
if reported_version >= script_version:
if ctx.game is not None and 'locations' in data_decoded:
# Not just a keep alive ping, parse
asyncio.create_task(parse_payload(data_decoded, ctx, False))
async_start(parse_payload(data_decoded, ctx, False))
if not ctx.auth:
ctx.auth = data_decoded['playerName']
if ctx.awaiting_rom:
@@ -255,17 +290,29 @@ async def run_game(romfile):
async def patch_and_run_game(apz5_file):
apz5_file = os.path.abspath(apz5_file)
base_name = os.path.splitext(apz5_file)[0]
decomp_path = base_name + '-decomp.z64'
comp_path = base_name + '.z64'
# Load vanilla ROM, patch file, compress ROM
rom = Rom(Utils.local_path(Utils.get_options()["oot_options"]["rom_file"]))
apply_patch_file(rom, apz5_file)
rom_file_name = Utils.get_options()["oot_options"]["rom_file"]
if not os.path.exists(rom_file_name):
rom_file_name = Utils.user_path(rom_file_name)
rom = Rom(rom_file_name)
sub_file = None
if zipfile.is_zipfile(apz5_file):
for name in zipfile.ZipFile(apz5_file).namelist():
if name.endswith('.zpf'):
sub_file = name
break
apply_patch_file(rom, apz5_file, sub_file=sub_file)
rom.write_to_file(decomp_path)
os.chdir(data_path("Compress"))
compress_rom_file(decomp_path, comp_path)
os.remove(decomp_path)
asyncio.create_task(run_game(comp_path))
async_start(run_game(comp_path))
if __name__ == '__main__':
@@ -281,7 +328,7 @@ if __name__ == '__main__':
if args.apz5_file:
logger.info("APZ5 file supplied, beginning patching process...")
asyncio.create_task(patch_and_run_game(args.apz5_file))
async_start(patch_and_run_game(args.apz5_file))
ctx = OoTContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="Server Loop")

View File

@@ -1,5 +1,7 @@
from __future__ import annotations
import abc
import logging
from copy import deepcopy
import math
import numbers
import typing
@@ -8,6 +10,11 @@ import random
from schema import Schema, And, Or, Optional
from Utils import get_fuzzy_results
if typing.TYPE_CHECKING:
from BaseClasses import PlandoOptions
from worlds.AutoWorld import World
import pathlib
class AssembleOptions(abc.ABCMeta):
def __new__(mcs, name, bases, attrs):
@@ -94,11 +101,11 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
supports_weighting = True
# filled by AssembleOptions:
name_lookup: typing.Dict[int, str]
name_lookup: typing.Dict[T, str]
options: typing.Dict[str, int]
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.get_current_option_name()})"
return f"{self.__class__.__name__}({self.current_option_name})"
def __hash__(self) -> int:
return hash(self.value)
@@ -108,7 +115,14 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
return self.name_lookup[self.value]
def get_current_option_name(self) -> str:
"""For display purposes."""
"""Deprecated. use current_option_name instead. TODO remove around 0.4"""
logging.warning(DeprecationWarning(f"get_current_option_name for {self.__class__.__name__} is deprecated."
f" use current_option_name instead. Worlds should use {self}.current_key"))
return self.current_option_name
@property
def current_option_name(self) -> str:
"""For display purposes. Worlds should be using current_key."""
return self.get_option_name(self.value)
@classmethod
@@ -125,21 +139,19 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
return bool(self.value)
@classmethod
@abc.abstractmethod
def from_any(cls, data: typing.Any) -> Option[T]:
raise NotImplementedError
...
if typing.TYPE_CHECKING:
from Generate import PlandoSettings
from worlds.AutoWorld import World
def verify(self, world: World, player_name: str, plando_options: PlandoSettings) -> None:
def verify(self, world: typing.Type[World], player_name: str, plando_options: PlandoOptions) -> None:
pass
else:
def verify(self, *args, **kwargs) -> None:
pass
class FreeText(Option):
class FreeText(Option[str]):
"""Text option that allows users to enter strings.
Needs to be validated by the world or option definition."""
@@ -160,11 +172,12 @@ class FreeText(Option):
return cls.from_text(str(data))
@classmethod
def get_option_name(cls, value: T) -> str:
def get_option_name(cls, value: str) -> str:
return value
class NumericOption(Option[int], numbers.Integral):
class NumericOption(Option[int], numbers.Integral, abc.ABC):
default = 0
# note: some of the `typing.Any`` here is a result of unresolved issue in python standards
# `int` is not a `numbers.Integral` according to the official typestubs
# (even though isinstance(5, numbers.Integral) == True)
@@ -421,19 +434,18 @@ class Choice(NumericOption):
class TextChoice(Choice):
"""Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string"""
value: typing.Union[str, int]
def __init__(self, value: typing.Union[str, int]):
assert isinstance(value, str) or isinstance(value, int), \
f"{value} is not a valid option for {self.__class__.__name__}"
self.value = value
super(TextChoice, self).__init__()
@property
def current_key(self) -> str:
if isinstance(self.value, str):
return self.value
else:
return self.name_lookup[self.value]
return super().current_key
@classmethod
def from_text(cls, text: str) -> TextChoice:
@@ -448,7 +460,7 @@ class TextChoice(Choice):
def get_option_name(cls, value: T) -> str:
if isinstance(value, str):
return value
return cls.name_lookup[value]
return super().get_option_name(value)
def __eq__(self, other: typing.Any):
if isinstance(other, self.__class__):
@@ -466,6 +478,123 @@ class TextChoice(Choice):
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
class BossMeta(AssembleOptions):
def __new__(mcs, name, bases, attrs):
if name != "PlandoBosses":
assert "bosses" in attrs, f"Please define valid bosses for {name}"
attrs["bosses"] = frozenset((boss.lower() for boss in attrs["bosses"]))
assert "locations" in attrs, f"Please define valid locations for {name}"
attrs["locations"] = frozenset((location.lower() for location in attrs["locations"]))
cls = super().__new__(mcs, name, bases, attrs)
assert not cls.duplicate_bosses or "singularity" in cls.options, f"Please define option_singularity for {name}"
return cls
class PlandoBosses(TextChoice, metaclass=BossMeta):
"""Generic boss shuffle option that supports plando. Format expected is
'location1-boss1;location2-boss2;shuffle_mode'.
If shuffle_mode is not provided in the string, this will be the default shuffle mode. Must override can_place_boss,
which passes a plando boss and location. Check if the placement is valid for your game here."""
bosses: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]]
locations: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]]
duplicate_bosses: bool = False
@classmethod
def from_text(cls, text: str):
# set all of our text to lower case for name checking
text = text.lower()
if text == "random":
return cls(random.choice(list(cls.options.values())))
for option_name, value in cls.options.items():
if option_name == text:
return cls(value)
options = text.split(";")
# since plando exists in the option verify the plando values given are valid
cls.validate_plando_bosses(options)
return cls.get_shuffle_mode(options)
@classmethod
def get_shuffle_mode(cls, option_list: typing.List[str]):
# find out what mode of boss shuffle we should use for placing bosses after plando
# and add as a string to look nice in the spoiler
if "random" in option_list:
shuffle = random.choice(list(cls.options))
option_list.remove("random")
options = ";".join(option_list) + f";{shuffle}"
boss_class = cls(options)
else:
for option in option_list:
if option in cls.options:
options = ";".join(option_list)
break
else:
if cls.duplicate_bosses and len(option_list) == 1:
if cls.valid_boss_name(option_list[0]):
# this doesn't exist in this class but it's a forced option for classes where this is called
options = option_list[0] + ";singularity"
else:
options = option_list[0] + f";{cls.name_lookup[cls.default]}"
else:
options = ";".join(option_list) + f";{cls.name_lookup[cls.default]}"
boss_class = cls(options)
return boss_class
@classmethod
def validate_plando_bosses(cls, options: typing.List[str]) -> None:
used_locations = []
used_bosses = []
for option in options:
# check if a shuffle mode was provided in the incorrect location
if option == "random" or option in cls.options:
if option != options[-1]:
raise ValueError(f"{option} option must be at the end of the boss_shuffle options!")
elif "-" in option:
location, boss = option.split("-")
if location in used_locations:
raise ValueError(f"Duplicate Boss Location {location} not allowed.")
if not cls.duplicate_bosses and boss in used_bosses:
raise ValueError(f"Duplicate Boss {boss} not allowed.")
used_locations.append(location)
used_bosses.append(boss)
if not cls.valid_boss_name(boss):
raise ValueError(f"{boss.title()} is not a valid boss name.")
if not cls.valid_location_name(location):
raise ValueError(f"{location.title()} is not a valid boss location name.")
if not cls.can_place_boss(boss, location):
raise ValueError(f"{location.title()} is not a valid location for {boss.title()} to be placed.")
else:
if cls.duplicate_bosses:
if not cls.valid_boss_name(option):
raise ValueError(f"{option} is not a valid boss name.")
else:
raise ValueError(f"{option.title()} is not formatted correctly.")
@classmethod
def can_place_boss(cls, boss: str, location: str) -> bool:
raise NotImplementedError
@classmethod
def valid_boss_name(cls, value: str) -> bool:
return value in cls.bosses
@classmethod
def valid_location_name(cls, value: str) -> bool:
return value in cls.locations
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
if isinstance(self.value, int):
return
from BaseClasses import PlandoOptions
if not(PlandoOptions.bosses & plando_options):
# plando is disabled but plando options were given so pull the option and change it to an int
option = self.value.split(";")[-1]
self.value = self.options[option]
logging.warning(f"The plando bosses module is turned off, so {self.name_lookup[self.value].title()} "
f"boss shuffle will be used for player {player_name}.")
class Range(NumericOption):
range_start = 0
range_end = 1
@@ -587,8 +716,16 @@ class SpecialRange(Range):
f"random-range-high-<min>-<max>, or random-range-<min>-<max>.")
class VerifyKeys:
valid_keys = frozenset()
class FreezeValidKeys(AssembleOptions):
def __new__(mcs, name, bases, attrs):
if "valid_keys" in attrs:
attrs["_valid_keys"] = frozenset(attrs["valid_keys"])
return super(FreezeValidKeys, mcs).__new__(mcs, name, bases, attrs)
class VerifyKeys(metaclass=FreezeValidKeys):
valid_keys: typing.Iterable = []
_valid_keys: frozenset # gets created by AssembleOptions from valid_keys
valid_keys_casefold: bool = False
convert_name_groups: bool = False
verify_item_name: bool = False
@@ -596,21 +733,26 @@ class VerifyKeys:
value: typing.Any
@classmethod
def verify_keys(cls, data):
def verify_keys(cls, data: typing.List[str]):
if cls.valid_keys:
data = set(data)
dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data)
extra = dataset - cls.valid_keys
extra = dataset - cls._valid_keys
if extra:
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
f"Allowed keys: {cls.valid_keys}.")
f"Allowed keys: {cls._valid_keys}.")
def verify(self, world, player_name: str, plando_options) -> None:
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
if self.convert_name_groups and self.verify_item_name:
new_value = type(self.value)() # empty container of whatever value is
for item_name in self.value:
new_value |= world.item_name_groups.get(item_name, {item_name})
self.value = new_value
elif self.convert_name_groups and self.verify_location_name:
new_value = type(self.value)()
for loc_name in self.value:
new_value |= world.location_name_groups.get(loc_name, {loc_name})
self.value = new_value
if self.verify_item_name:
for item_name in self.value:
if item_name not in world.item_names:
@@ -628,11 +770,11 @@ class VerifyKeys:
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys):
default = {}
default: typing.Dict[str, typing.Any] = {}
supports_weighting = False
def __init__(self, value: typing.Dict[str, typing.Any]):
self.value = value
self.value = deepcopy(value)
@classmethod
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
@@ -659,11 +801,15 @@ class ItemDict(OptionDict):
class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
default = []
# Supports duplicate entries and ordering.
# If only unique entries are needed and input order of elements does not matter, OptionSet should be used instead.
# Not a docstring so it doesn't get grabbed by the options system.
default: typing.List[typing.Any] = []
supports_weighting = False
def __init__(self, value: typing.List[typing.Any]):
self.value = value or []
self.value = deepcopy(value)
super(OptionList, self).__init__()
@classmethod
@@ -685,11 +831,11 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
class OptionSet(Option[typing.Set[str]], VerifyKeys):
default = frozenset()
default: typing.Union[typing.Set[str], typing.FrozenSet[str]] = frozenset()
supports_weighting = False
def __init__(self, value: typing.Union[typing.Set[str, typing.Any], typing.List[str, typing.Any]]):
self.value = set(value)
def __init__(self, value: typing.Iterable[str]):
self.value = set(deepcopy(value))
super(OptionSet, self).__init__()
@classmethod
@@ -710,7 +856,9 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
return item in self.value
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
class ItemSet(OptionSet):
verify_item_name = True
convert_name_groups = True
class Accessibility(Choice):
@@ -728,7 +876,7 @@ class Accessibility(Choice):
class ProgressionBalancing(SpecialRange):
"""A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
[0-99, default 50] A lower setting means more getting stuck. A higher setting means less getting stuck."""
A lower setting means more getting stuck. A higher setting means less getting stuck."""
default = 50
range_start = 0
range_end = 99
@@ -746,11 +894,6 @@ common_options = {
}
class ItemSet(OptionSet):
verify_item_name = True
convert_name_groups = True
class LocalItems(ItemSet):
"""Forces these items to be in their native world."""
display_name = "Local Items"
@@ -767,27 +910,36 @@ class StartInventory(ItemDict):
display_name = "Start Inventory"
class StartInventoryPool(StartInventory):
"""Start with these items and don't place them in the world.
The game decides what the replacement items will be."""
verify_item_name = True
display_name = "Start Inventory from Pool"
class StartHints(ItemSet):
"""Start with these item's locations prefilled into the !hint command."""
display_name = "Start Hints"
class StartLocationHints(OptionSet):
class LocationSet(OptionSet):
verify_location_name = True
convert_name_groups = True
class StartLocationHints(LocationSet):
"""Start with these locations and their item prefilled into the !hint command"""
display_name = "Start Location Hints"
verify_location_name = True
class ExcludeLocations(OptionSet):
class ExcludeLocations(LocationSet):
"""Prevent these locations from having an important item"""
display_name = "Excluded Locations"
verify_location_name = True
class PriorityLocations(OptionSet):
class PriorityLocations(LocationSet):
"""Prevent these locations from having an unimportant item"""
display_name = "Priority Locations"
verify_location_name = True
class DeathLink(Toggle):
@@ -805,7 +957,8 @@ class ItemLinks(OptionList):
Optional("exclude"): [And(str, len)],
"replacement_item": Or(And(str, len), None),
Optional("local_items"): [And(str, len)],
Optional("non_local_items"): [And(str, len)]
Optional("non_local_items"): [And(str, len)],
Optional("link_replacement"): Or(None, bool),
}
])
@@ -827,7 +980,8 @@ class ItemLinks(OptionList):
pool |= {item_name}
return pool
def verify(self, world, player_name: str, plando_options) -> None:
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
link: dict
super(ItemLinks, self).verify(world, player_name, plando_options)
existing_links = set()
for link in self.value:
@@ -852,7 +1006,9 @@ class ItemLinks(OptionList):
intersection = local_items.intersection(non_local_items)
if intersection:
raise Exception(f"item_link {link['name']} has {intersection} items in both its local_items and non_local_items pool.")
raise Exception(f"item_link {link['name']} has {intersection} "
f"items in both its local_items and non_local_items pool.")
link.setdefault("link_replacement", None)
per_game_common_options = {
@@ -867,6 +1023,64 @@ per_game_common_options = {
"item_links": ItemLinks
}
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True):
import os
import yaml
from jinja2 import Template
from worlds import AutoWorldRegister
from Utils import local_path, __version__
full_path: str
os.makedirs(target_folder, exist_ok=True)
# clean out old
for file in os.listdir(target_folder):
full_path = os.path.join(target_folder, file)
if os.path.isfile(full_path) and full_path.endswith(".yaml"):
os.unlink(full_path)
def dictify_range(option: typing.Union[Range, SpecialRange]):
data = {option.default: 50}
for sub_option in ["random", "random-low", "random-high"]:
if sub_option != option.default:
data[sub_option] = 0
notes = {}
for name, number in getattr(option, "special_range_names", {}).items():
notes[name] = f"equivalent to {number}"
if number in data:
data[name] = data[number]
del data[number]
else:
data[name] = 0
return data, notes
for game_name, world in AutoWorldRegister.world_types.items():
if not world.hidden or generate_hidden:
all_options: typing.Dict[str, AssembleOptions] = {
**per_game_common_options,
**world.option_definitions
}
with open(local_path("data", "options.yaml")) as f:
file_data = f.read()
res = Template(file_data).render(
options=all_options,
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
dictify_range=dictify_range,
)
del file_data
with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f:
f.write(res)
if __name__ == "__main__":
from worlds.alttp.Options import Logic

428
Patch.py
View File

@@ -1,266 +1,23 @@
from __future__ import annotations
import shutil
import json
import bsdiff4
import yaml
import os
import lzma
import threading
import concurrent.futures
import zipfile
import sys
from typing import Tuple, Optional, Dict, Any, Union, BinaryIO
from typing import Tuple, Optional, TypedDict
import ModuleUpdate
ModuleUpdate.update()
if __name__ == "__main__":
import ModuleUpdate
ModuleUpdate.update()
import Utils
current_patch_version = 5
from worlds.Files import AutoPatchRegister, APDeltaPatch
class AutoPatchRegister(type):
patch_types: Dict[str, APDeltaPatch] = {}
file_endings: Dict[str, APDeltaPatch] = {}
def __new__(cls, name: str, bases, dct: Dict[str, Any]):
# construct class
new_class = super().__new__(cls, name, bases, dct)
if "game" in dct:
AutoPatchRegister.patch_types[dct["game"]] = new_class
if not dct["patch_file_ending"]:
raise Exception(f"Need an expected file ending for {name}")
AutoPatchRegister.file_endings[dct["patch_file_ending"]] = new_class
return new_class
@staticmethod
def get_handler(file: str) -> Optional[type(APDeltaPatch)]:
for file_ending, handler in AutoPatchRegister.file_endings.items():
if file.endswith(file_ending):
return handler
class APContainer:
"""A zipfile containing at least archipelago.json"""
version: int = current_patch_version
compression_level: int = 9
compression_method: int = zipfile.ZIP_DEFLATED
game: Optional[str] = None
# instance attributes:
path: Optional[str]
class RomMeta(TypedDict):
server: str
player: Optional[int]
player_name: str
server: str
def __init__(self, path: Optional[str] = None, player: Optional[int] = None,
player_name: str = "", server: str = ""):
self.path = path
self.player = player
self.player_name = player_name
self.server = server
def write(self, file: Optional[Union[str, BinaryIO]] = None):
if not self.path and not file:
raise FileNotFoundError(f"Cannot write {self.__class__.__name__} due to no path provided.")
with zipfile.ZipFile(file if file else self.path, "w", self.compression_method, True, self.compression_level) \
as zf:
if file:
self.path = zf.filename
self.write_contents(zf)
def write_contents(self, opened_zipfile: zipfile.ZipFile):
manifest = self.get_manifest()
try:
manifest = json.dumps(manifest)
except Exception as e:
raise Exception(f"Manifest {manifest} did not convert to json.") from e
else:
opened_zipfile.writestr("archipelago.json", manifest)
def read(self, file: Optional[Union[str, BinaryIO]] = None):
"""Read data into patch object. file can be file-like, such as an outer zip file's stream."""
if not self.path and not file:
raise FileNotFoundError(f"Cannot read {self.__class__.__name__} due to no path provided.")
with zipfile.ZipFile(file if file else self.path, "r") as zf:
if file:
self.path = zf.filename
self.read_contents(zf)
def read_contents(self, opened_zipfile: zipfile.ZipFile):
with opened_zipfile.open("archipelago.json", "r") as f:
manifest = json.load(f)
if manifest["compatible_version"] > self.version:
raise Exception(f"File (version: {manifest['compatible_version']}) too new "
f"for this handler (version: {self.version})")
self.player = manifest["player"]
self.server = manifest["server"]
self.player_name = manifest["player_name"]
def get_manifest(self) -> dict:
return {
"server": self.server, # allow immediate connection to server in multiworld. Empty string otherwise
"player": self.player,
"player_name": self.player_name,
"game": self.game,
# minimum version of patch system expected for patching to be successful
"compatible_version": 4,
"version": current_patch_version,
}
class APDeltaPatch(APContainer, metaclass=AutoPatchRegister):
"""An APContainer that additionally has delta.bsdiff4
containing a delta patch to get the desired file, often a rom."""
hash = Optional[str] # base checksum of source file
patch_file_ending: str = ""
delta: Optional[bytes] = None
result_file_ending: str = ".sfc"
source_data: bytes
def __init__(self, *args, patched_path: str = "", **kwargs):
self.patched_path = patched_path
super(APDeltaPatch, self).__init__(*args, **kwargs)
def get_manifest(self) -> dict:
manifest = super(APDeltaPatch, self).get_manifest()
manifest["base_checksum"] = self.hash
manifest["result_file_ending"] = self.result_file_ending
manifest["patch_file_ending"] = self.patch_file_ending
return manifest
@classmethod
def get_source_data(cls) -> bytes:
"""Get Base data"""
raise NotImplementedError()
@classmethod
def get_source_data_with_cache(cls) -> bytes:
if not hasattr(cls, "source_data"):
cls.source_data = cls.get_source_data()
return cls.source_data
def write_contents(self, opened_zipfile: zipfile.ZipFile):
super(APDeltaPatch, self).write_contents(opened_zipfile)
# write Delta
opened_zipfile.writestr("delta.bsdiff4",
bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read()),
compress_type=zipfile.ZIP_STORED) # bsdiff4 is a format with integrated compression
def read_contents(self, opened_zipfile: zipfile.ZipFile):
super(APDeltaPatch, self).read_contents(opened_zipfile)
self.delta = opened_zipfile.read("delta.bsdiff4")
def patch(self, target: str):
"""Base + Delta -> Patched"""
if not self.delta:
self.read()
result = bsdiff4.patch(self.get_source_data_with_cache(), self.delta)
with open(target, "wb") as f:
f.write(result)
# legacy patch handling follows:
GAME_ALTTP = "A Link to the Past"
GAME_SM = "Super Metroid"
GAME_SOE = "Secret of Evermore"
GAME_SMZ3 = "SMZ3"
GAME_DKC3 = "Donkey Kong Country 3"
supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore", "SMZ3", "Donkey Kong Country 3"}
preferred_endings = {
GAME_ALTTP: "apbp",
GAME_SM: "apm3",
GAME_SOE: "apsoe",
GAME_SMZ3: "apsmz",
GAME_DKC3: "apdkc3"
}
def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes:
if game == GAME_ALTTP:
from worlds.alttp.Rom import LTTPJPN10HASH as HASH
elif game == GAME_SM:
from worlds.sm.Rom import SMJUHASH as HASH
elif game == GAME_SOE:
from worlds.soe.Patch import USHASH as HASH
elif game == GAME_SMZ3:
from worlds.alttp.Rom import LTTPJPN10HASH as ALTTPHASH
from worlds.sm.Rom import SMJUHASH as SMHASH
HASH = ALTTPHASH + SMHASH
elif game == GAME_DKC3:
from worlds.dkc3.Rom import USHASH as HASH
else:
raise RuntimeError(f"Selected game {game} for base rom not found.")
patch = yaml.dump({"meta": metadata,
"patch": patch,
"game": game,
# minimum version of patch system expected for patching to be successful
"compatible_version": 3,
"version": current_patch_version,
"base_checksum": HASH})
return patch.encode(encoding="utf-8-sig")
def generate_patch(rom: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes:
if metadata is None:
metadata = {}
patch = bsdiff4.diff(get_base_rom_data(game), rom)
return generate_yaml(patch, metadata, game)
def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str = None,
player: int = 0, player_name: str = "", game: str = GAME_ALTTP) -> str:
meta = {"server": server, # allow immediate connection to server in multiworld. Empty string otherwise
"player_id": player,
"player_name": player_name}
bytes = generate_patch(load_bytes(rom_file_to_patch),
meta,
game)
target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + (
".apbp" if game == GAME_ALTTP
else ".apsmz" if game == GAME_SMZ3
else ".apdkc3" if game == GAME_DKC3
else ".apm3")
write_lzma(bytes, target)
return target
def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]:
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
game_name = data["game"]
if not ignore_version and data["compatible_version"] > current_patch_version:
raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.")
patched_data = bsdiff4.patch(get_base_rom_data(game_name), data["patch"])
rom_hash = patched_data[int(0x7FC0):int(0x7FD5)]
data["meta"]["hash"] = "".join(chr(x) for x in rom_hash)
target = os.path.splitext(patch_file)[0] + ".sfc"
return data["meta"], target, patched_data
def get_base_rom_data(game: str):
if game == GAME_ALTTP:
from worlds.alttp.Rom import get_base_rom_bytes
elif game == "alttp": # old version for A Link to the Past
from worlds.alttp.Rom import get_base_rom_bytes
elif game == GAME_SM:
from worlds.sm.Rom import get_base_rom_bytes
elif game == GAME_SOE:
from worlds.soe.Patch import get_base_rom_path
get_base_rom_bytes = lambda: bytes(read_rom(open(get_base_rom_path(), "rb")))
elif game == GAME_SMZ3:
from worlds.smz3.Rom import get_base_rom_bytes
elif game == GAME_DKC3:
from worlds.dkc3.Rom import get_base_rom_bytes
else:
raise RuntimeError("Selected game for base rom not found.")
return get_base_rom_bytes()
def create_rom_file(patch_file: str) -> Tuple[dict, str]:
def create_rom_file(patch_file: str) -> Tuple[RomMeta, str]:
auto_handler = AutoPatchRegister.get_handler(patch_file)
if auto_handler:
handler: APDeltaPatch = auto_handler(patch_file)
@@ -269,171 +26,10 @@ def create_rom_file(patch_file: str) -> Tuple[dict, str]:
return {"server": handler.server,
"player": handler.player,
"player_name": handler.player_name}, target
else:
data, target, patched_data = create_rom_bytes(patch_file)
with open(target, "wb") as f:
f.write(patched_data)
return data, target
def update_patch_data(patch_data: bytes, server: str = "") -> bytes:
data = Utils.parse_yaml(lzma.decompress(patch_data).decode("utf-8-sig"))
data["meta"]["server"] = server
bytes = generate_yaml(data["patch"], data["meta"], data["game"])
return lzma.compress(bytes)
def load_bytes(path: str) -> bytes:
with open(path, "rb") as f:
return f.read()
def write_lzma(data: bytes, path: str):
with lzma.LZMAFile(path, 'wb') as f:
f.write(data)
def read_rom(stream, strip_header=True) -> bytearray:
"""Reads rom into bytearray and optionally strips off any smc header"""
buffer = bytearray(stream.read())
if strip_header and len(buffer) % 0x400 == 0x200:
return buffer[0x200:]
return buffer
raise NotImplementedError(f"No Handler for {patch_file} found.")
if __name__ == "__main__":
host = Utils.get_public_ipv4()
options = Utils.get_options()['server_options']
if options['host']:
host = options['host']
address = f"{host}:{options['port']}"
ziplock = threading.Lock()
print(f"Host for patches to be created is {address}")
with concurrent.futures.ThreadPoolExecutor() as pool:
for rom in sys.argv:
try:
if rom.endswith(".sfc"):
print(f"Creating patch for {rom}")
result = pool.submit(create_patch_file, rom, address)
result.add_done_callback(lambda task: print(f"Created patch {task.result()}"))
elif rom.endswith(".apbp"):
print(f"Applying patch {rom}")
data, target = create_rom_file(rom)
#romfile, adjusted = Utils.get_adjuster_settings(target)
adjuster_settings = Utils.get_adjuster_settings(GAME_ALTTP)
adjusted = False
if adjuster_settings:
import pprint
from worlds.alttp.Rom import get_base_rom_path
adjuster_settings.rom = target
adjuster_settings.baserom = get_base_rom_path()
adjuster_settings.world = None
whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
"uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes",
"reduceflashing", "deathlink"}
printed_options = {name: value for name, value in vars(adjuster_settings).items() if name in whitelist}
if hasattr(adjuster_settings, "sprite_pool"):
sprite_pool = {}
for sprite in getattr(adjuster_settings, "sprite_pool"):
if sprite in sprite_pool:
sprite_pool[sprite] += 1
else:
sprite_pool[sprite] = 1
if sprite_pool:
printed_options["sprite_pool"] = sprite_pool
adjust_wanted = str('no')
if not hasattr(adjuster_settings, 'auto_apply') or 'ask' in adjuster_settings.auto_apply:
adjust_wanted = input(f"Last used adjuster settings were found. Would you like to apply these? \n"
f"{pprint.pformat(printed_options)}\n"
f"Enter yes, no, always or never: ")
if adjuster_settings.auto_apply == 'never': # never adjust, per user request
adjust_wanted = 'no'
elif adjuster_settings.auto_apply == 'always':
adjust_wanted = 'yes'
if adjust_wanted and "never" in adjust_wanted:
adjuster_settings.auto_apply = 'never'
Utils.persistent_store("adjuster", GAME_ALTTP, adjuster_settings)
elif adjust_wanted and "always" in adjust_wanted:
adjuster_settings.auto_apply = 'always'
Utils.persistent_store("adjuster", GAME_ALTTP, adjuster_settings)
if adjust_wanted and adjust_wanted.startswith("y"):
if hasattr(adjuster_settings, "sprite_pool"):
from LttPAdjuster import AdjusterWorld
adjuster_settings.world = AdjusterWorld(getattr(adjuster_settings, "sprite_pool"))
adjusted = True
import LttPAdjuster
_, romfile = LttPAdjuster.adjust(adjuster_settings)
if hasattr(adjuster_settings, "world"):
delattr(adjuster_settings, "world")
else:
adjusted = False
if adjusted:
try:
shutil.move(romfile, target)
romfile = target
except Exception as e:
print(e)
print(f"Created rom {romfile if adjusted else target}.")
if 'server' in data:
Utils.persistent_store("servers", data['hash'], data['server'])
print(f"Host is {data['server']}")
elif rom.endswith(".apm3"):
print(f"Applying patch {rom}")
data, target = create_rom_file(rom)
print(f"Created rom {target}.")
if 'server' in data:
Utils.persistent_store("servers", data['hash'], data['server'])
print(f"Host is {data['server']}")
elif rom.endswith(".apsmz"):
print(f"Applying patch {rom}")
data, target = create_rom_file(rom)
print(f"Created rom {target}.")
if 'server' in data:
Utils.persistent_store("servers", data['hash'], data['server'])
print(f"Host is {data['server']}")
elif rom.endswith(".apdkc3"):
print(f"Applying patch {rom}")
data, target = create_rom_file(rom)
print(f"Created rom {target}.")
if 'server' in data:
Utils.persistent_store("servers", data['hash'], data['server'])
print(f"Host is {data['server']}")
elif rom.endswith(".zip"):
print(f"Updating host in patch files contained in {rom}")
def _handle_zip_file_entry(zfinfo: zipfile.ZipInfo, server: str):
data = zfr.read(zfinfo)
if zfinfo.filename.endswith(".apbp") or \
zfinfo.filename.endswith(".apm3") or \
zfinfo.filename.endswith(".apdkc3"):
data = update_patch_data(data, server)
with ziplock:
zfw.writestr(zfinfo, data)
return zfinfo.filename
futures = []
with zipfile.ZipFile(rom, "r") as zfr:
updated_zip = os.path.splitext(rom)[0] + "_updated.zip"
with zipfile.ZipFile(updated_zip, "w", compression=zipfile.ZIP_DEFLATED,
compresslevel=9) as zfw:
for zfname in zfr.namelist():
futures.append(pool.submit(_handle_zip_file_entry, zfr.getinfo(zfname), address))
for future in futures:
print(f"File {future.result()} added to {os.path.split(updated_zip)[1]}")
except:
import traceback
traceback.print_exc()
input("Press enter to close.")
for file in sys.argv[1:]:
meta_data, result_file = create_rom_file(file)
print(f"Patch with meta-data {meta_data} was written to {result_file}")

351
PokemonClient.py Normal file
View File

@@ -0,0 +1,351 @@
import asyncio
import json
import time
import os
import bsdiff4
import subprocess
import zipfile
from asyncio import StreamReader, StreamWriter
from typing import List
import Utils
from Utils import async_start
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
get_base_parser
from worlds.pokemon_rb.locations import location_data
from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}}
location_bytes_bits = {}
for location in location_data:
if location.ram_address is not None:
if type(location.ram_address) == list:
location_map[type(location.ram_address).__name__][(location.ram_address[0].flag, location.ram_address[1].flag)] = location.address
location_bytes_bits[location.address] = [{'byte': location.ram_address[0].byte, 'bit': location.ram_address[0].bit},
{'byte': location.ram_address[1].byte, 'bit': location.ram_address[1].bit}]
else:
location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address
location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit}
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart pkmn_rb.lua"
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure pkmn_rb.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart pkmn_rb.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
DISPLAY_MSGS = True
SCRIPT_VERSION = 3
class GBCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx: CommonContext):
super().__init__(ctx)
def _cmd_gb(self):
"""Check Gameboy Connection State"""
if isinstance(self.ctx, GBContext):
logger.info(f"Gameboy Status: {self.ctx.gb_status}")
class GBContext(CommonContext):
command_processor = GBCommandProcessor
game = 'Pokemon Red and Blue'
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.gb_streams: (StreamReader, StreamWriter) = None
self.gb_sync_task = None
self.messages = {}
self.locations_array = None
self.gb_status = CONNECTION_INITIAL_STATUS
self.awaiting_rom = False
self.display_msgs = True
self.deathlink_pending = False
self.set_deathlink = False
self.client_compatibility_mode = 0
self.items_handling = 0b001
self.sent_release = False
self.sent_collect = False
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(GBContext, self).server_auth(password_requested)
if not self.auth:
self.awaiting_rom = True
logger.info('Awaiting connection to EmuHawk to get Player information')
return
await self.send_connect()
def _set_message(self, msg: str, msg_id: int):
if DISPLAY_MSGS:
self.messages[(time.time(), msg_id)] = msg
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
self.locations_array = None
if 'death_link' in args['slot_data'] and args['slot_data']['death_link']:
self.set_deathlink = True
elif cmd == "RoomInfo":
self.seed_name = args['seed_name']
elif cmd == 'Print':
msg = args['text']
if ': !' not in msg:
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "ReceivedItems":
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
self._set_message(msg, SYSTEM_MESSAGE_ID)
def on_deathlink(self, data: dict):
self.deathlink_pending = True
super().on_deathlink(data)
def run_gui(self):
from kvui import GameManager
class GBManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Pokémon Client"
self.ui = GBManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def get_payload(ctx: GBContext):
current_time = time.time()
ret = json.dumps(
{
"items": [item.item for item in ctx.items_received],
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
if key[0] > current_time - 10},
"deathlink": ctx.deathlink_pending,
"options": ((ctx.permissions['release'] in ('goal', 'enabled')) * 2) + (ctx.permissions['collect'] in ('goal', 'enabled'))
}
)
ctx.deathlink_pending = False
return ret
async def parse_locations(data: List, ctx: GBContext):
locations = []
flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20],
"Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E],
"Rod": data[0x140 + 0x20 + 0x0E:0x140 + 0x20 + 0x0E + 0x01]}
if len(data) > 0x140 + 0x20 + 0x0E + 0x01:
flags["DexSanityFlag"] = data[0x140 + 0x20 + 0x0E + 0x01:]
else:
flags["DexSanityFlag"] = [0] * 19
for flag_type, loc_map in location_map.items():
for flag, loc_id in loc_map.items():
if flag_type == "list":
if (flags["EventFlag"][location_bytes_bits[loc_id][0]['byte']] & 1 << location_bytes_bits[loc_id][0]['bit']
and flags["Missable"][location_bytes_bits[loc_id][1]['byte']] & 1 << location_bytes_bits[loc_id][1]['bit']):
locations.append(loc_id)
elif flags[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']:
locations.append(loc_id)
if flags["EventFlag"][280] & 1 and not ctx.finished_game:
await ctx.send_msgs([
{"cmd": "StatusUpdate",
"status": 30}
])
ctx.finished_game = True
if locations == ctx.locations_array:
return
ctx.locations_array = locations
if locations is not None:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}])
async def gb_sync_task(ctx: GBContext):
logger.info("Starting GB connector. Use /gb for status information")
while not ctx.exit_event.is_set():
error_status = None
if ctx.gb_streams:
(reader, writer) = ctx.gb_streams
msg = get_payload(ctx).encode()
writer.write(msg)
writer.write(b'\n')
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
# Data will return a dict with up to two fields:
# 1. A keepalive response of the Players Name (always)
# 2. An array representing the memory values of the locations area (if in game)
data = await asyncio.wait_for(reader.readline(), timeout=5)
data_decoded = json.loads(data.decode())
if 'scriptVersion' not in data_decoded or data_decoded['scriptVersion'] != SCRIPT_VERSION:
msg = "You are connecting with an incompatible Lua script version. Ensure your connector Lua " \
"and PokemonClient are from the same Archipelago installation."
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
ctx.client_compatibility_mode = data_decoded['clientCompatibilityVersion']
if ctx.client_compatibility_mode == 0:
ctx.items_handling = 0b101 # old patches will not have local start inventory, must be requested
if ctx.seed_name and ctx.seed_name != ''.join([chr(i) for i in data_decoded['seedName'] if i != 0]):
msg = "The server is running a different multiworld than your client is. (invalid seed_name)"
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
ctx.seed_name = ''.join([chr(i) for i in data_decoded['seedName'] if i != 0])
if not ctx.auth:
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
if ctx.auth == '':
msg = "Invalid ROM detected. No player name built into the ROM."
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
if ctx.awaiting_rom:
await ctx.server_auth(False)
if 'locations' in data_decoded and ctx.game and ctx.gb_status == CONNECTION_CONNECTED_STATUS \
and not error_status and ctx.auth:
# Not just a keep alive ping, parse
async_start(parse_locations(data_decoded['locations'], ctx))
if 'deathLink' in data_decoded and data_decoded['deathLink'] and 'DeathLink' in ctx.tags:
await ctx.send_death(ctx.auth + " is out of usable Pokémon! " + ctx.auth + " blacked out!")
if 'options' in data_decoded:
msgs = []
if data_decoded['options'] & 4 and not ctx.sent_release:
ctx.sent_release = True
msgs.append({"cmd": "Say", "text": "!release"})
if data_decoded['options'] & 8 and not ctx.sent_collect:
ctx.sent_collect = True
msgs.append({"cmd": "Say", "text": "!collect"})
if msgs:
await ctx.send_msgs(msgs)
if ctx.set_deathlink:
await ctx.update_death_link(True)
except asyncio.TimeoutError:
logger.debug("Read Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.gb_streams = None
except ConnectionResetError as e:
logger.debug("Read failed due to Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.gb_streams = None
except TimeoutError:
logger.debug("Connection Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.gb_streams = None
except ConnectionResetError:
logger.debug("Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.gb_streams = None
if ctx.gb_status == CONNECTION_TENTATIVE_STATUS:
if not error_status:
logger.info("Successfully Connected to Gameboy")
ctx.gb_status = CONNECTION_CONNECTED_STATUS
else:
ctx.gb_status = f"Was tentatively connected but error occured: {error_status}"
elif error_status:
ctx.gb_status = error_status
logger.info("Lost connection to Gameboy and attempting to reconnect. Use /gb for status updates")
else:
try:
logger.debug("Attempting to connect to Gameboy")
ctx.gb_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 17242), timeout=10)
ctx.gb_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
ctx.gb_status = CONNECTION_TIMING_OUT_STATUS
continue
except ConnectionRefusedError:
logger.debug("Connection Refused, Trying Again")
ctx.gb_status = CONNECTION_REFUSED_STATUS
continue
async def run_game(romfile):
auto_start = Utils.get_options()["pokemon_rb_options"].get("rom_start", True)
if auto_start is True:
import webbrowser
webbrowser.open(romfile)
elif os.path.isfile(auto_start):
subprocess.Popen([auto_start, romfile],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
async def patch_and_run_game(game_version, patch_file, ctx):
base_name = os.path.splitext(patch_file)[0]
comp_path = base_name + '.gb'
if game_version == "blue":
delta_patch = BlueDeltaPatch
else:
delta_patch = RedDeltaPatch
try:
base_rom = delta_patch.get_source_data()
except Exception as msg:
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
with zipfile.ZipFile(patch_file, 'r') as patch_archive:
with patch_archive.open('delta.bsdiff4', 'r') as stream:
patch = stream.read()
patched_rom_data = bsdiff4.patch(base_rom, patch)
with open(comp_path, "wb") as patched_rom_file:
patched_rom_file.write(patched_rom_data)
async_start(run_game(comp_path))
if __name__ == '__main__':
Utils.init_logging("PokemonClient")
options = Utils.get_options()
async def main():
parser = get_base_parser()
parser.add_argument('patch_file', default="", type=str, nargs="?",
help='Path to an APRED or APBLUE patch file')
args = parser.parse_args()
ctx = GBContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
ctx.gb_sync_task = asyncio.create_task(gb_sync_task(ctx), name="GB Sync")
if args.patch_file:
ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower()
if ext == "apred":
logger.info("APRED file supplied, beginning patching process...")
async_start(patch_and_run_game("red", args.patch_file, ctx))
elif ext == "apblue":
logger.info("APBLUE file supplied, beginning patching process...")
async_start(patch_and_run_game("blue", args.patch_file, ctx))
else:
logger.warning(f"Unknown patch file extension {ext}")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.gb_sync_task:
await ctx.gb_sync_task
import colorama
colorama.init()
asyncio.run(main())
colorama.deinit()

View File

@@ -28,6 +28,27 @@ Currently, the following games are supported:
* Starcraft 2: Wings of Liberty
* Donkey Kong Country 3
* Dark Souls 3
* Super Mario World
* Pokémon Red and Blue
* Hylics 2
* Overcooked! 2
* Zillion
* Lufia II Ancient Cave
* Blasphemous
* Wargroove
* Stardew Valley
* The Legend of Zelda
* The Messenger
* Kingdom Hearts 2
* The Legend of Zelda: Link's Awakening DX
* Clique
* Adventure
* DLC Quest
* Noita
* Undertale
* Bumper Stickers
* Mega Man Battle Network 3: Blue Version
* Muse Dash
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

File diff suppressed because it is too large Load Diff

View File

@@ -10,23 +10,13 @@ import re
import sys
import typing
import queue
import zipfile
import io
from pathlib import Path
import nest_asyncio
import sc2
from sc2.bot_ai import BotAI
from sc2.data import Race
from sc2.main import run_game
from sc2.player import Bot
import NetUtils
from MultiServer import mark_raw
# CommonClient import first to trigger ModuleUpdater
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
from Utils import init_logging, is_windows
from worlds.sc2wol import SC2WoLWorld
from worlds.sc2wol.Items import lookup_id_to_name, item_table, ItemData, type_flaggroups
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
from worlds.sc2wol.MissionTables import lookup_id_to_mission
from worlds.sc2wol.Regions import MissionInfo
if __name__ == "__main__":
init_logging("SC2Client", exception_logger="Client")
@@ -34,10 +24,20 @@ if __name__ == "__main__":
logger = logging.getLogger("Client")
sc2_logger = logging.getLogger("Starcraft2")
import colorama
import nest_asyncio
from worlds._sc2common import bot
from worlds._sc2common.bot.data import Race
from worlds._sc2common.bot.main import run_game
from worlds._sc2common.bot.player import Bot
from worlds.sc2wol import SC2WoLWorld
from worlds.sc2wol.Items import lookup_id_to_name, item_table, ItemData, type_flaggroups
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
from worlds.sc2wol.MissionTables import lookup_id_to_mission
from worlds.sc2wol.Regions import MissionInfo
from NetUtils import ClientStatus, RawJSONtoTextParser
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
import colorama
from NetUtils import ClientStatus, NetworkItem, RawJSONtoTextParser
from MultiServer import mark_raw
nest_asyncio.apply()
max_bonus: int = 8
@@ -51,9 +51,9 @@ class StarcraftClientProcessor(ClientCommandProcessor):
"""Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal"""
options = difficulty.split()
num_options = len(options)
difficulty_choice = options[0].lower()
if num_options > 0:
difficulty_choice = options[0].lower()
if difficulty_choice == "casual":
self.ctx.difficulty_override = 0
elif difficulty_choice == "normal":
@@ -70,7 +70,11 @@ class StarcraftClientProcessor(ClientCommandProcessor):
return True
else:
self.output("Difficulty needs to be specified in the command.")
if self.ctx.difficulty == -1:
self.output("Please connect to a seed before checking difficulty.")
else:
self.output("Current difficulty: " + ["Casual", "Normal", "Hard", "Brutal"][self.ctx.difficulty])
self.output("To change the difficulty, add the name of the difficulty after the command.")
return False
def _cmd_disable_mission_check(self) -> bool:
@@ -115,12 +119,40 @@ class StarcraftClientProcessor(ClientCommandProcessor):
"""Manually set the SC2 install directory (if the automatic detection fails)."""
if path:
os.environ["SC2PATH"] = path
check_mod_install()
is_mod_installed_correctly()
return True
else:
sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.")
return False
def _cmd_download_data(self) -> bool:
"""Download the most recent release of the necessary files for playing SC2 with
Archipelago. Will overwrite existing files."""
if "SC2PATH" not in os.environ:
check_game_install_path()
if os.path.exists(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt"):
with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "r") as f:
current_ver = f.read()
else:
current_ver = None
tempzip, version = download_latest_release_zip('TheCondor07', 'Starcraft2ArchipelagoData',
current_version=current_ver, force_download=True)
if tempzip != '':
try:
zipfile.ZipFile(tempzip).extractall(path=os.environ["SC2PATH"])
sc2_logger.info(f"Download complete. Version {version} installed.")
with open(os.environ["SC2PATH"]+"ArchipelagoSC2Version.txt", "w") as f:
f.write(version)
finally:
os.remove(tempzip)
else:
sc2_logger.warning("Download aborted/failed. Read the log for more information.")
return False
return True
class SC2Context(CommonContext):
command_processor = StarcraftClientProcessor
@@ -128,7 +160,9 @@ class SC2Context(CommonContext):
items_handling = 0b111
difficulty = -1
all_in_choice = 0
mission_order = 0
mission_req_table: typing.Dict[str, MissionInfo] = {}
final_mission: int = 29
announcements = queue.Queue()
sc2_run_task: typing.Optional[asyncio.Task] = None
missions_unlocked: bool = False # allow launching missions ignoring requirements
@@ -153,16 +187,29 @@ 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"]
# Maintaining backwards compatibility with older slot data
self.mission_req_table = {
mission: MissionInfo(**slot_req_table[mission]) for mission in slot_req_table
mission: MissionInfo(
**{field: value for field, value in mission_info.items() if field in MissionInfo._fields}
)
for mission, mission_info in slot_req_table.items()
}
self.mission_order = args["slot_data"].get("mission_order", 0)
self.final_mission = args["slot_data"].get("final_mission", 29)
self.build_location_to_mission_mapping()
# 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()
# Looks for the required maps and mods for SC2. Runs check_game_install_path.
maps_present = is_mod_installed_correctly()
if os.path.exists(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt"):
with open(os.environ["SC2PATH"] + "ArchipelagoSC2Version.txt", "r") as f:
current_ver = f.read()
if is_mod_update_available("TheCondor07", "Starcraft2ArchipelagoData", current_ver):
sc2_logger.info("NOTICE: Update for required files found. Run /download_data to install.")
elif maps_present:
sc2_logger.warning("NOTICE: Your map files may be outdated (version number not found). "
"Run /download_data to update them.")
def on_print_json(self, args: dict):
# goes to this world
@@ -192,8 +239,6 @@ class SC2Context(CommonContext):
from kivy.uix.floatlayout import FloatLayout
from kivy.properties import StringProperty
import Utils
class HoverableButton(HoverBehavior, Button):
pass
@@ -274,7 +319,6 @@ class SC2Context(CommonContext):
self.refresh_from_launching = True
self.mission_panel.clear_widgets()
if self.ctx.mission_req_table:
self.last_checked_locations = self.ctx.checked_locations.copy()
self.first_check = False
@@ -292,17 +336,20 @@ class SC2Context(CommonContext):
for category in categories:
category_panel = MissionCategory()
if category.startswith('_'):
category_display_name = ''
else:
category_display_name = category
category_panel.add_widget(
Label(text=category, size_hint_y=None, height=50, outline_width=1))
Label(text=category_display_name, size_hint_y=None, height=50, outline_width=1))
for mission in categories[category]:
text: str = mission
tooltip: str = ""
mission_id: int = self.ctx.mission_req_table[mission].id
# Map has uncollected locations
if mission in unfinished_missions:
text = f"[color=6495ED]{text}[/color]"
elif mission in available_missions:
text = f"[color=FFFFFF]{text}[/color]"
# Map requirements not met
@@ -321,6 +368,16 @@ class SC2Context(CommonContext):
remaining_location_names: typing.List[str] = [
self.ctx.location_names[loc] for loc in self.ctx.locations_for_mission(mission)
if loc in self.ctx.missing_locations]
if mission_id == self.ctx.final_mission:
if mission in available_missions:
text = f"[color=FFBC95]{mission}[/color]"
else:
text = f"[color=D0C0BE]{mission}[/color]"
if tooltip:
tooltip += "\n"
tooltip += "Final Mission"
if remaining_location_names:
if tooltip:
tooltip += "\n"
@@ -330,7 +387,7 @@ class SC2Context(CommonContext):
mission_button = MissionButton(text=text, size_hint_y=None, height=50)
mission_button.tooltip_text = tooltip
mission_button.bind(on_press=self.mission_callback)
self.mission_id_to_button[self.ctx.mission_req_table[mission].id] = mission_button
self.mission_id_to_button[mission_id] = mission_button
category_panel.add_widget(mission_button)
category_panel.add_widget(Label(text=""))
@@ -357,8 +414,9 @@ class SC2Context(CommonContext):
self.ui = SC2Manager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
Builder.load_file(Utils.local_path(os.path.dirname(SC2WoLWorld.__file__), "Starcraft2.kv"))
import pkgutil
data = pkgutil.get_data(SC2WoLWorld.__module__, "Starcraft2.kv").decode()
Builder.load_string(data)
async def shutdown(self):
await super(SC2Context, self).shutdown()
@@ -438,10 +496,13 @@ wol_default_categories = [
"Rebellion", "Rebellion", "Rebellion", "Rebellion", "Rebellion", "Prophecy", "Prophecy", "Prophecy", "Prophecy",
"Char", "Char", "Char", "Char"
]
wol_default_category_names = [
"Mar Sara", "Colonist", "Artifact", "Covert", "Rebellion", "Prophecy", "Char"
]
def calculate_items(items: typing.List[NetUtils.NetworkItem]) -> typing.List[int]:
network_item: NetUtils.NetworkItem
def calculate_items(items: typing.List[NetworkItem]) -> typing.List[int]:
network_item: NetworkItem
accumulators: typing.List[int] = [0 for _ in type_flaggroups]
for network_item in items:
@@ -480,11 +541,11 @@ async def starcraft_launch(ctx: SC2Context, mission_id: int):
sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.")
with DllDirectory(None):
run_game(sc2.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
run_game(bot.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
name="Archipelago", fullscreen=True)], realtime=True)
class ArchipelagoBot(sc2.bot_ai.BotAI):
class ArchipelagoBot(bot.bot_ai.BotAI):
game_running: bool = False
mission_completed: bool = False
boni: typing.List[bool]
@@ -555,7 +616,7 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
if self.can_read_game:
if game_state & (1 << 1) and not self.mission_completed:
if self.mission_id != 29:
if self.mission_id != self.ctx.final_mission:
print("Mission Completed")
await self.ctx.send_msgs(
[{"cmd": 'LocationChecks',
@@ -585,6 +646,13 @@ def request_unfinished_missions(ctx: SC2Context):
_, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks)
# Removing All-In from location pool
final_mission = lookup_id_to_mission[ctx.final_mission]
if final_mission in unfinished_missions.keys():
message = f"Final Mission Available: {final_mission}[{ctx.final_mission}]\n" + message
if unfinished_missions[final_mission] == -1:
unfinished_missions.pop(final_mission)
message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[mission].id}] " +
mark_up_objectives(
f"[{len(unfinished_missions[mission])}/"
@@ -711,13 +779,14 @@ def calc_available_missions(ctx: SC2Context, unlocks=None):
return available_missions
def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete):
def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete: int):
"""Returns a bool signifying if the mission has all requirements complete and can be done
Arguments:
ctx -- instance of SC2Context
locations_to_check -- the mission string name to check
missions_complete -- an int of how many missions have been completed
mission_path -- a list of missions that have already been checked
"""
if len(ctx.mission_req_table[mission_name].required_world) >= 1:
# A check for when the requirements are being or'd
@@ -735,7 +804,18 @@ def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete
else:
req_success = False
# Grid-specific logic (to avoid long path checks and infinite recursion)
if ctx.mission_order in (3, 4):
if req_success:
return True
else:
if req_mission is ctx.mission_req_table[mission_name].required_world[-1]:
return False
else:
continue
# Recursively check required mission to see if it's requirements are met, in case !collect has been done
# Skipping recursive check on Grid settings to speed up checks and avoid infinite recursion
if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete):
if not ctx.mission_req_table[mission_name].or_requirements:
return False
@@ -784,7 +864,7 @@ def check_game_install_path() -> bool:
documentspath = buf.value
einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt"))
else:
einfo = str(sc2.paths.get_home() / Path(sc2.paths.USERPATH[sc2.paths.PF]))
einfo = str(bot.paths.get_home() / Path(bot.paths.USERPATH[bot.paths.PF]))
# Check if the file exists.
if os.path.isfile(einfo):
@@ -800,7 +880,7 @@ def check_game_install_path() -> bool:
f"try again.")
return False
if os.path.exists(base):
executable = sc2.paths.latest_executeble(Path(base).expanduser() / "Versions")
executable = bot.paths.latest_executeble(Path(base).expanduser() / "Versions")
# Finally, check the path for an actual executable.
# If we find one, great. Set up the SC2PATH.
@@ -820,18 +900,53 @@ def check_game_install_path() -> bool:
return False
def check_mod_install() -> bool:
# Pull up the SC2PATH if set. If not, encourage the user to manually run /set_path.
try:
# Check inside the Mods folder for Archipelago.SC2Mod. If found, tell user. If not, tell user.
if os.path.isfile(modfile := (os.environ["SC2PATH"] / Path("Mods") / Path("Archipelago.SC2Mod"))):
sc2_logger.info(f"Archipelago mod found at {modfile}.")
return True
else:
sc2_logger.warning(f"Archipelago mod could not be found at {modfile}. Please install the mod file there.")
except KeyError:
sc2_logger.warning(f"SC2PATH isn't set. Please run /set_path with the path to your SC2 install.")
return False
def is_mod_installed_correctly() -> bool:
"""Searches for all required files."""
if "SC2PATH" not in os.environ:
check_game_install_path()
mapdir = os.environ['SC2PATH'] / Path('Maps/ArchipelagoCampaign')
modfile = os.environ["SC2PATH"] / Path("Mods/Archipelago.SC2Mod")
wol_required_maps = [
"ap_thanson01.SC2Map", "ap_thanson02.SC2Map", "ap_thanson03a.SC2Map", "ap_thanson03b.SC2Map",
"ap_thorner01.SC2Map", "ap_thorner02.SC2Map", "ap_thorner03.SC2Map", "ap_thorner04.SC2Map", "ap_thorner05s.SC2Map",
"ap_traynor01.SC2Map", "ap_traynor02.SC2Map", "ap_traynor03.SC2Map",
"ap_ttosh01.SC2Map", "ap_ttosh02.SC2Map", "ap_ttosh03a.SC2Map", "ap_ttosh03b.SC2Map",
"ap_ttychus01.SC2Map", "ap_ttychus02.SC2Map", "ap_ttychus03.SC2Map", "ap_ttychus04.SC2Map", "ap_ttychus05.SC2Map",
"ap_tvalerian01.SC2Map", "ap_tvalerian02a.SC2Map", "ap_tvalerian02b.SC2Map", "ap_tvalerian03.SC2Map",
"ap_tzeratul01.SC2Map", "ap_tzeratul02.SC2Map", "ap_tzeratul03.SC2Map", "ap_tzeratul04.SC2Map"
]
needs_files = False
# Check for maps.
missing_maps = []
for mapfile in wol_required_maps:
if not os.path.isfile(mapdir / mapfile):
missing_maps.append(mapfile)
if len(missing_maps) >= 19:
sc2_logger.warning(f"All map files missing from {mapdir}.")
needs_files = True
elif len(missing_maps) > 0:
for map in missing_maps:
sc2_logger.debug(f"Missing {map} from {mapdir}.")
sc2_logger.warning(f"Missing {len(missing_maps)} map files.")
needs_files = True
else: # Must be no maps missing
sc2_logger.info(f"All maps found in {mapdir}.")
# Check for mods.
if os.path.isfile(modfile):
sc2_logger.info(f"Archipelago mod found at {modfile}.")
else:
sc2_logger.warning(f"Archipelago mod could not be found at {modfile}.")
needs_files = True
# Final verdict.
if needs_files:
sc2_logger.warning(f"Required files are missing. Run /download_data to acquire them.")
return False
else:
return True
class DllDirectory:
@@ -870,6 +985,64 @@ class DllDirectory:
return False
def download_latest_release_zip(owner: str, repo: str, current_version: str = None, force_download=False) -> (str, str):
"""Downloads the latest release of a GitHub repo to the current directory as a .zip file."""
import requests
headers = {"Accept": 'application/vnd.github.v3+json'}
url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
r1 = requests.get(url, headers=headers)
if r1.status_code == 200:
latest_version = r1.json()["tag_name"]
sc2_logger.info(f"Latest version: {latest_version}.")
else:
sc2_logger.warning(f"Status code: {r1.status_code}")
sc2_logger.warning(f"Failed to reach GitHub. Could not find download link.")
sc2_logger.warning(f"text: {r1.text}")
return "", current_version
if (force_download is False) and (current_version == latest_version):
sc2_logger.info("Latest version already installed.")
return "", current_version
sc2_logger.info(f"Attempting to download version {latest_version} of {repo}.")
download_url = r1.json()["assets"][0]["browser_download_url"]
r2 = requests.get(download_url, headers=headers)
if r2.status_code == 200 and zipfile.is_zipfile(io.BytesIO(r2.content)):
with open(f"{repo}.zip", "wb") as fh:
fh.write(r2.content)
sc2_logger.info(f"Successfully downloaded {repo}.zip.")
return f"{repo}.zip", latest_version
else:
sc2_logger.warning(f"Status code: {r2.status_code}")
sc2_logger.warning("Download failed.")
sc2_logger.warning(f"text: {r2.text}")
return "", current_version
def is_mod_update_available(owner: str, repo: str, current_version: str) -> bool:
import requests
headers = {"Accept": 'application/vnd.github.v3+json'}
url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
r1 = requests.get(url, headers=headers)
if r1.status_code == 200:
latest_version = r1.json()["tag_name"]
if current_version != latest_version:
return True
else:
return False
else:
sc2_logger.warning(f"Failed to reach GitHub while checking for updates.")
sc2_logger.warning(f"Status code: {r1.status_code}")
sc2_logger.warning(f"text: {r1.text}")
return False
if __name__ == '__main__':
colorama.init()
asyncio.run(main())

498
UndertaleClient.py Normal file
View File

@@ -0,0 +1,498 @@
from __future__ import annotations
import os
import asyncio
import typing
import bsdiff4
import shutil
import Utils
from NetUtils import NetworkItem, ClientStatus
from worlds import undertale
from MultiServer import mark_raw
from CommonClient import CommonContext, server_loop, \
gui_enabled, ClientCommandProcessor, get_base_parser
from Utils import async_start
class UndertaleCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx):
super().__init__(ctx)
def _cmd_resync(self):
"""Manually trigger a resync."""
if isinstance(self.ctx, UndertaleContext):
self.output(f"Syncing items.")
self.ctx.syncing = True
def _cmd_patch(self):
"""Patch the game."""
if isinstance(self.ctx, UndertaleContext):
os.makedirs(name=os.getcwd() + "\\Undertale", exist_ok=True)
self.ctx.patch_game()
self.output("Patched.")
@mark_raw
def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
"""Patch the game automatically."""
if isinstance(self.ctx, UndertaleContext):
os.makedirs(name=os.getcwd() + "\\Undertale", exist_ok=True)
tempInstall = steaminstall
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
tempInstall = None
if tempInstall is None:
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
if not os.path.exists("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"):
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
elif not os.path.exists(tempInstall):
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
if not os.path.exists("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"):
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
if not os.path.exists(tempInstall) or not os.path.exists(tempInstall) or not os.path.isfile(os.path.join(tempInstall, "data.win")):
self.output("ERROR: Cannot find Undertale. Please rerun the command with the correct folder."
" command. \"/auto_patch (Steam directory)\".")
else:
for file_name in os.listdir(tempInstall):
if file_name != "steam_api.dll":
shutil.copy(tempInstall+"\\"+file_name,
os.getcwd() + "\\Undertale\\" + file_name)
self.ctx.patch_game()
self.output("Patching successful!")
def _cmd_online(self):
"""Makes you no longer able to see other Undertale players."""
if isinstance(self.ctx, UndertaleContext):
self.ctx.update_online_mode(not ("Online" in self.ctx.tags))
if "Online" in self.ctx.tags:
self.output(f"Now online.")
else:
self.output(f"Now offline.")
def _cmd_deathlink(self):
"""Toggles deathlink"""
if isinstance(self.ctx, UndertaleContext):
self.ctx.deathlink_status = not self.ctx.deathlink_status
if self.ctx.deathlink_status:
self.output(f"Deathlink enabled.")
else:
self.output(f"Deathlink disabled.")
class UndertaleContext(CommonContext):
tags = {"AP", "Online"}
game = "Undertale"
command_processor = UndertaleCommandProcessor
items_handling = 0b111
route = None
pieces_needed = None
completed_routes = None
completed_count = 0
save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.pieces_needed = 0
self.game = "Undertale"
self.got_deathlink = False
self.syncing = False
self.deathlink_status = False
self.tem_armor = False
self.completed_count = 0
self.completed_routes = {"pacifist": 0, "genocide": 0, "neutral": 0}
def patch_game(self):
with open(os.getcwd() + "/Undertale/data.win", "rb") as f:
patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff"))
with open(os.getcwd() + "/Undertale/data.win", "wb") as f:
f.write(patchedFile)
os.makedirs(name=os.getcwd() + "\\Undertale\\" + "Custom Sprites", exist_ok=True)
with open(os.path.expandvars(os.getcwd() + "\\Undertale\\" + "Custom Sprites\\" +
"Which Character.txt"), "w") as f:
f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only "
"line other than this one.\n", "frisk"])
f.close()
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super().server_auth(password_requested)
await self.get_username()
await self.send_connect()
def clear_undertale_files(self):
path = self.save_game_folder
self.finished_game = False
for root, dirs, files in os.walk(path):
for file in files:
if "check.spot" == file or "scout" == file:
os.remove(os.path.join(root, file))
elif file.endswith((".item", ".victory", ".route", ".playerspot", ".mad",
".youDied", ".LV", ".mine", ".flag", ".hint")):
os.remove(os.path.join(root, file))
async def connect(self, address: typing.Optional[str] = None):
self.clear_undertale_files()
await super().connect(address)
async def disconnect(self, allow_autoreconnect: bool = False):
self.clear_undertale_files()
await super().disconnect(allow_autoreconnect)
async def connection_closed(self):
self.clear_undertale_files()
await super().connection_closed()
async def shutdown(self):
self.clear_undertale_files()
await super().shutdown()
def update_online_mode(self, online):
old_tags = self.tags.copy()
if online:
self.tags.add("Online")
else:
self.tags -= {"Online"}
if old_tags != self.tags and self.server and not self.server.socket.closed:
async_start(self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}]))
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
self.game = self.slot_info[self.slot].game
async_start(process_undertale_cmd(self, cmd, args))
def run_gui(self):
from kvui import GameManager
class UTManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Undertale Client"
self.ui = UTManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def on_deathlink(self, data: typing.Dict[str, typing.Any]):
self.got_deathlink = True
super().on_deathlink(data)
def to_room_name(place_name: str):
if place_name == "Old Home Exit":
return "room_ruinsexit"
elif place_name == "Snowdin Forest":
return "room_tundra1"
elif place_name == "Snowdin Town Exit":
return "room_fogroom"
elif place_name == "Waterfall":
return "room_water1"
elif place_name == "Waterfall Exit":
return "room_fire2"
elif place_name == "Hotland":
return "room_fire_prelab"
elif place_name == "Hotland Exit":
return "room_fire_precore"
elif place_name == "Core":
return "room_fire_core1"
async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
if cmd == "Connected":
if not os.path.exists(ctx.save_game_folder):
os.mkdir(ctx.save_game_folder)
ctx.route = args["slot_data"]["route"]
ctx.pieces_needed = args["slot_data"]["key_pieces"]
ctx.tem_armor = args["slot_data"]["temy_armor_include"]
await ctx.send_msgs([{"cmd": "Get", "keys": [str(ctx.slot)+" RoutesDone neutral",
str(ctx.slot)+" RoutesDone pacifist",
str(ctx.slot)+" RoutesDone genocide"]}])
await ctx.send_msgs([{"cmd": "SetNotify", "keys": [str(ctx.slot)+" RoutesDone neutral",
str(ctx.slot)+" RoutesDone pacifist",
str(ctx.slot)+" RoutesDone genocide"]}])
if args["slot_data"]["only_flakes"]:
with open(os.path.join(ctx.save_game_folder, "GenoNoChest.flag"), "w") as f:
f.close()
if not args["slot_data"]["key_hunt"]:
ctx.pieces_needed = 0
if args["slot_data"]["rando_love"]:
filename = f"LOVErando.LV"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
f.close()
if args["slot_data"]["rando_stats"]:
filename = f"STATrando.LV"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
f.close()
filename = f"{ctx.route}.route"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
f.close()
filename = f"check.spot"
with open(os.path.join(ctx.save_game_folder, filename), "a") as f:
for ss in ctx.checked_locations:
f.write(str(ss-12000)+"\n")
f.close()
elif cmd == "LocationInfo":
for l in args["locations"]:
locationid = l.location
filename = f"{str(locationid-12000)}.hint"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
toDraw = ""
for i in range(20):
if i < len(str(ctx.item_names[l.item])):
toDraw += str(ctx.item_names[l.item])[i]
else:
break
f.write(toDraw)
f.close()
elif cmd == "Retrieved":
if str(ctx.slot)+" RoutesDone neutral" in args["keys"]:
if args["keys"][str(ctx.slot)+" RoutesDone neutral"] is not None:
ctx.completed_routes["neutral"] = args["keys"][str(ctx.slot)+" RoutesDone neutral"]
if str(ctx.slot)+" RoutesDone genocide" in args["keys"]:
if args["keys"][str(ctx.slot)+" RoutesDone genocide"] is not None:
ctx.completed_routes["genocide"] = args["keys"][str(ctx.slot)+" RoutesDone genocide"]
if str(ctx.slot)+" RoutesDone pacifist" in args["keys"]:
if args["keys"][str(ctx.slot) + " RoutesDone pacifist"] is not None:
ctx.completed_routes["pacifist"] = args["keys"][str(ctx.slot)+" RoutesDone pacifist"]
elif cmd == "SetReply":
if args["value"] is not None:
if str(ctx.slot)+" RoutesDone pacifist" == args["key"]:
ctx.completed_routes["pacifist"] = args["value"]
elif str(ctx.slot)+" RoutesDone genocide" == args["key"]:
ctx.completed_routes["genocide"] = args["value"]
elif str(ctx.slot)+" RoutesDone neutral" == args["key"]:
ctx.completed_routes["neutral"] = args["value"]
elif cmd == "ReceivedItems":
start_index = args["index"]
if start_index == 0:
ctx.items_received = []
elif start_index != len(ctx.items_received):
sync_msg = [{"cmd": "Sync"}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks",
"locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
if start_index == len(ctx.items_received):
counter = -1
placedWeapon = 0
placedArmor = 0
for item in args["items"]:
id = NetworkItem(*item).location
while NetworkItem(*item).location < 0 and \
counter <= id:
id -= 1
if NetworkItem(*item).location < 0:
counter -= 1
filename = f"{str(id)}PLR{str(NetworkItem(*item).player)}.item"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
if NetworkItem(*item).item == 77701:
if placedWeapon == 0:
f.write(str(77013-11000))
elif placedWeapon == 1:
f.write(str(77014-11000))
elif placedWeapon == 2:
f.write(str(77025-11000))
elif placedWeapon == 3:
f.write(str(77045-11000))
elif placedWeapon == 4:
f.write(str(77049-11000))
elif placedWeapon == 5:
f.write(str(77047-11000))
elif placedWeapon == 6:
if str(ctx.route) == "genocide" or str(ctx.route) == "all_routes":
f.write(str(77052-11000))
else:
f.write(str(77051-11000))
else:
f.write(str(77003-11000))
placedWeapon += 1
elif NetworkItem(*item).item == 77702:
if placedArmor == 0:
f.write(str(77012-11000))
elif placedArmor == 1:
f.write(str(77015-11000))
elif placedArmor == 2:
f.write(str(77024-11000))
elif placedArmor == 3:
f.write(str(77044-11000))
elif placedArmor == 4:
f.write(str(77048-11000))
elif placedArmor == 5:
if str(ctx.route) == "genocide":
f.write(str(77053-11000))
else:
f.write(str(77046-11000))
elif placedArmor == 6 and ((not str(ctx.route) == "genocide") or ctx.tem_armor):
if str(ctx.route) == "all_routes":
f.write(str(77053-11000))
elif str(ctx.route) == "genocide":
f.write(str(77064-11000))
else:
f.write(str(77050-11000))
elif placedArmor == 7 and ctx.tem_armor and not str(ctx.route) == "genocide":
f.write(str(77064-11000))
else:
f.write(str(77004-11000))
placedArmor += 1
else:
f.write(str(NetworkItem(*item).item-11000))
f.close()
ctx.items_received.append(NetworkItem(*item))
if [item.item for item in ctx.items_received].count(77000) >= ctx.pieces_needed > 0:
filename = f"{str(-99999)}PLR{str(0)}.item"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
f.write(str(77787 - 11000))
f.close()
filename = f"{str(-99998)}PLR{str(0)}.item"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
f.write(str(77789 - 11000))
f.close()
ctx.watcher_event.set()
elif cmd == "RoomUpdate":
if "checked_locations" in args:
filename = f"check.spot"
with open(os.path.join(ctx.save_game_folder, filename), "a") as f:
for ss in ctx.checked_locations:
f.write(str(ss-12000)+"\n")
f.close()
elif cmd == "Bounced":
tags = args.get("tags", [])
if "Online" in tags:
data = args.get("worlds/undertale/data", {})
if data["player"] != ctx.slot and data["player"] is not None:
filename = f"FRISK" + str(data["player"]) + ".playerspot"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
f.write(str(data["x"]) + str(data["y"]) + str(data["room"]) + str(
data["spr"]) + str(data["frm"]))
f.close()
async def multi_watcher(ctx: UndertaleContext):
while not ctx.exit_event.is_set():
path = ctx.save_game_folder
for root, dirs, files in os.walk(path):
for file in files:
if "spots.mine" in file and "Online" in ctx.tags:
with open(root + "/" + file, "r") as mine:
this_x = mine.readline()
this_y = mine.readline()
this_room = mine.readline()
this_sprite = mine.readline()
this_frame = mine.readline()
mine.close()
message = [{"cmd": "Bounce", "tags": ["Online"],
"data": {"player": ctx.slot, "x": this_x, "y": this_y, "room": this_room,
"spr": this_sprite, "frm": this_frame}}]
await ctx.send_msgs(message)
await asyncio.sleep(0.1)
async def game_watcher(ctx: UndertaleContext):
while not ctx.exit_event.is_set():
await ctx.update_death_link(ctx.deathlink_status)
path = ctx.save_game_folder
if ctx.syncing:
for root, dirs, files in os.walk(path):
for file in files:
if ".item" in file:
os.remove(root+"/"+file)
sync_msg = [{"cmd": "Sync"}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
ctx.syncing = False
if ctx.got_deathlink:
ctx.got_deathlink = False
with open(os.path.join(ctx.save_game_folder, "/WelcomeToTheDead.youDied"), "w") as f:
f.close()
sending = []
victory = False
found_routes = 0
for root, dirs, files in os.walk(path):
for file in files:
if "DontBeMad.mad" in file and "DeathLink" in ctx.tags:
os.remove(root+"/"+file)
await ctx.send_death()
if "scout" == file:
sending = []
with open(root+"/"+file, "r") as f:
lines = f.readlines()
for l in lines:
if ctx.server_locations.__contains__(int(l)+12000):
sending = sending + [int(l)+12000]
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": sending,
"create_as_hint": int(2)}])
os.remove(root+"/"+file)
if "check.spot" in file:
sending = []
with open(root+"/"+file, "r") as f:
lines = f.readlines()
for l in lines:
sending = sending+[(int(l))+12000]
message = [{"cmd": "LocationChecks", "locations": sending}]
await ctx.send_msgs(message)
if "victory" in file and str(ctx.route) in file:
victory = True
if ".playerspot" in file and "Online" not in ctx.tags:
os.remove(root+"/"+file)
if "victory" in file:
if str(ctx.route) == "all_routes":
if "neutral" in file and ctx.completed_routes["neutral"] != 1:
await ctx.send_msgs([{"cmd": "Set", "key": str(ctx.slot)+" RoutesDone neutral",
"default": 0, "want_reply": True, "operations": [{"operation": "max",
"value": 1}]}])
elif "pacifist" in file and ctx.completed_routes["pacifist"] != 1:
await ctx.send_msgs([{"cmd": "Set", "key": str(ctx.slot)+" RoutesDone pacifist",
"default": 0, "want_reply": True, "operations": [{"operation": "max",
"value": 1}]}])
elif "genocide" in file and ctx.completed_routes["genocide"] != 1:
await ctx.send_msgs([{"cmd": "Set", "key": str(ctx.slot)+" RoutesDone genocide",
"default": 0, "want_reply": True, "operations": [{"operation": "max",
"value": 1}]}])
if str(ctx.route) == "all_routes":
found_routes += ctx.completed_routes["neutral"]
found_routes += ctx.completed_routes["pacifist"]
found_routes += ctx.completed_routes["genocide"]
if str(ctx.route) == "all_routes" and found_routes >= 3:
victory = True
ctx.locations_checked = sending
if (not ctx.finished_game) and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
await asyncio.sleep(0.1)
def main():
Utils.init_logging("UndertaleClient", exception_logger="Client")
async def _main():
ctx = UndertaleContext(None, None)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
asyncio.create_task(
game_watcher(ctx), name="UndertaleProgressionWatcher")
asyncio.create_task(
multi_watcher(ctx), name="UndertaleMultiplayerWatcher")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
await ctx.exit_event.wait()
await ctx.shutdown()
import colorama
colorama.init()
asyncio.run(_main())
colorama.deinit()
if __name__ == "__main__":
parser = get_base_parser(description="Undertale Client, for text interfacing.")
args = parser.parse_args()
main()

270
Utils.py
View File

@@ -1,5 +1,7 @@
from __future__ import annotations
import asyncio
import json
import typing
import builtins
import os
@@ -11,6 +13,8 @@ import io
import collections
import importlib
import logging
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
from yaml import load, load_all, dump, SafeLoader
try:
@@ -34,8 +38,11 @@ class Version(typing.NamedTuple):
minor: int
build: int
def as_simple_string(self) -> str:
return ".".join(str(item) for item in self)
__version__ = "0.3.5"
__version__ = "0.4.2"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")
@@ -84,7 +91,10 @@ def is_frozen() -> bool:
def local_path(*path: str) -> str:
"""Returns path to a file in the local Archipelago installation or source."""
"""
Returns path to a file in the local Archipelago installation or source.
This might be read-only and user_path should be used instead for ROMs, configuration, etc.
"""
if hasattr(local_path, 'cached_path'):
pass
elif is_frozen():
@@ -96,7 +106,7 @@ def local_path(*path: str) -> str:
local_path.cached_path = os.path.dirname(os.path.abspath(sys.argv[0]))
else:
import __main__
if hasattr(__main__, "__file__"):
if hasattr(__main__, "__file__") and os.path.isfile(__main__.__file__):
# we are running in a normal Python environment
local_path.cached_path = os.path.dirname(os.path.abspath(__main__.__file__))
else:
@@ -139,7 +149,18 @@ def user_path(*path: str) -> str:
return os.path.join(user_path.cached_path, *path)
def output_path(*path: str):
def cache_path(*path: str) -> str:
"""Returns path to a file in the user's Archipelago cache directory."""
if hasattr(cache_path, "cached_path"):
pass
else:
import platformdirs
cache_path.cached_path = platformdirs.user_cache_dir("Archipelago", False)
return os.path.join(cache_path.cached_path, *path)
def output_path(*path: str) -> str:
if hasattr(output_path, 'cached_path'):
return os.path.join(output_path.cached_path, *path)
output_path.cached_path = user_path(get_options()["general_options"]["output_path"])
@@ -192,11 +213,11 @@ def get_public_ipv4() -> str:
ip = socket.gethostbyname(socket.gethostname())
ctx = get_cert_none_ssl_context()
try:
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx).read().decode("utf8").strip()
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip()
except Exception as e:
# noinspection PyBroadException
try:
ip = urllib.request.urlopen("https://v4.ident.me", context=ctx).read().decode("utf8").strip()
ip = urllib.request.urlopen("https://v4.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
except Exception:
logging.exception(e)
pass # we could be offline, in a local game, so no point in erroring out
@@ -210,15 +231,18 @@ def get_public_ipv6() -> str:
ip = socket.gethostbyname(socket.gethostname())
ctx = get_cert_none_ssl_context()
try:
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx).read().decode("utf8").strip()
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
except Exception as e:
logging.exception(e)
pass # we could be offline, in a local game, or ipv6 may not be available
return ip
OptionsType = typing.Dict[str, typing.Dict[str, typing.Any]]
@cache_argsless
def get_default_options() -> dict:
def get_default_options() -> OptionsType:
# Refer to host.yaml for comments as to what all these options mean.
options = {
"general_options": {
@@ -226,20 +250,24 @@ def get_default_options() -> dict:
},
"factorio_options": {
"executable": os.path.join("factorio", "bin", "x64", "factorio"),
"filter_item_sends": False,
"bridge_chat_out": True,
},
"sni_options": {
"sni_path": "SNI",
"snes_rom_start": True,
},
"sm_options": {
"rom_file": "Super Metroid (JU).sfc",
"sni": "SNI",
"rom_start": True,
},
"soe_options": {
"rom_file": "Secret of Evermore (USA).sfc",
},
"lttp_options": {
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
"sni": "SNI",
"rom_start": True,
},
"ladx_options": {
"rom_file": "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc",
},
"server_options": {
"host": None,
@@ -253,7 +281,7 @@ def get_default_options() -> dict:
"disable_item_cheat": False,
"location_check_points": 1,
"hint_cost": 10,
"forfeit_mode": "goal",
"release_mode": "goal",
"collect_mode": "disabled",
"remaining_mode": "goal",
"auto_shutdown": 0,
@@ -261,13 +289,12 @@ def get_default_options() -> dict:
"log_network": 0
},
"generator": {
"teams": 1,
"enemizer_path": os.path.join("EnemizerCLI", "EnemizerCLI.Core"),
"player_files_path": "Players",
"players": 0,
"weights_file_path": "weights.yaml",
"meta_file_path": "meta.yaml",
"spoiler": 2,
"spoiler": 3,
"glitch_triforce_room": 1,
"race": 0,
"plando_options": "bosses",
@@ -279,18 +306,54 @@ def get_default_options() -> dict:
},
"oot_options": {
"rom_file": "The Legend of Zelda - Ocarina of Time.z64",
"rom_start": True
},
"dkc3_options": {
"rom_file": "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc",
"sni": "SNI",
},
"smw_options": {
"rom_file": "Super Mario World (USA).sfc",
},
"zillion_options": {
"rom_file": "Zillion (UE) [!].sms",
# RetroArch doesn't make it easy to launch a game from the command line.
# You have to know the path to the emulator core library on the user's computer.
"rom_start": "retroarch",
},
"pokemon_rb_options": {
"red_rom_file": "Pokemon Red (UE) [S][!].gb",
"blue_rom_file": "Pokemon Blue (UE) [S][!].gb",
"rom_start": True
},
"ffr_options": {
"display_msgs": True,
},
"lufia2ac_options": {
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
},
"tloz_options": {
"rom_file": "Legend of Zelda, The (U) (PRG0) [!].nes",
"rom_start": True,
"display_msgs": True,
},
"wargroove_options": {
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
},
"mmbn3_options": {
"rom_file": "Mega Man Battle Network 3 - Blue Version (USA).gba",
"rom_start": True
},
"adventure_options": {
"rom_file": "ADVNTURE.BIN",
"display_msgs": True,
"rom_start": True,
"rom_args": ""
},
}
return options
def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
def update_options(src: dict, dest: dict, filename: str, keys: list) -> OptionsType:
for key, value in src.items():
new_keys = keys.copy()
new_keys.append(key)
@@ -310,9 +373,9 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
@cache_argsless
def get_options() -> dict:
def get_options() -> OptionsType:
filenames = ("options.yaml", "host.yaml")
locations = []
locations: typing.List[str] = []
if os.path.join(os.getcwd()) != local_path():
locations += filenames # use files from cwd only if it's not the local_path
locations += [user_path(filename) for filename in filenames]
@@ -353,7 +416,46 @@ def persistent_load() -> typing.Dict[str, dict]:
return storage
def get_adjuster_settings(game_name: str):
def get_file_safe_name(name: str) -> str:
return "".join(c for c in name if c not in '<>:"/\\|?*')
def load_data_package_for_checksum(game: str, checksum: typing.Optional[str]) -> Dict[str, Any]:
if checksum and game:
if checksum != get_file_safe_name(checksum):
raise ValueError(f"Bad symbols in checksum: {checksum}")
path = cache_path("datapackage", get_file_safe_name(game), f"{checksum}.json")
if os.path.exists(path):
try:
with open(path, "r", encoding="utf-8-sig") as f:
return json.load(f)
except Exception as e:
logging.debug(f"Could not load data package: {e}")
# fall back to old cache
cache = persistent_load().get("datapackage", {}).get("games", {}).get(game, {})
if cache.get("checksum") == checksum:
return cache
# cache does not match
return {}
def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> None:
checksum = data.get("checksum")
if checksum and game:
if checksum != get_file_safe_name(checksum):
raise ValueError(f"Bad symbols in checksum: {checksum}")
game_folder = cache_path("datapackage", get_file_safe_name(game))
os.makedirs(game_folder, exist_ok=True)
try:
with open(os.path.join(game_folder, f"{checksum}.json"), "w", encoding="utf-8-sig") as f:
json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
except Exception as e:
logging.debug(f"Could not store data package: {e}")
def get_adjuster_settings(game_name: str) -> typing.Dict[str, typing.Any]:
adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {})
return adjuster_settings
@@ -392,7 +494,8 @@ class RestrictedUnpickler(pickle.Unpickler):
# Options and Plando are unpickled by WebHost -> Generate
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:
return getattr(self.generic_properties_module, name)
if module.endswith("Options"):
# pep 8 specifies that modules should have "all-lowercase names" (options, not Options)
if module.lower().endswith("options"):
if module == "Options":
mod = self.options_module
else:
@@ -409,6 +512,15 @@ def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()
class ByValue:
"""
Mixin for enums to pickle value instead of name (restores pre-3.11 behavior). Use as left-most parent.
See https://github.com/python/cpython/pull/26658 for why this exists.
"""
def __reduce_ex__(self, prot):
return self.__class__, (self._value_, )
class KeyedDefaultDict(collections.defaultdict):
"""defaultdict variant that uses the missing key as argument to default_factory"""
default_factory: typing.Callable[[typing.Any], typing.Any]
@@ -432,6 +544,7 @@ loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': log
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w",
log_format: str = "[%(name)s at %(asctime)s]: %(message)s",
exception_logger: typing.Optional[str] = None):
import datetime
loglevel: int = loglevel_mapping.get(loglevel, loglevel)
log_folder = user_path("logs")
os.makedirs(log_folder, exist_ok=True)
@@ -440,6 +553,9 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
root_logger.removeHandler(handler)
handler.close()
root_logger.setLevel(loglevel)
logging.getLogger("websockets").setLevel(loglevel) # make sure level is applied for websockets
if "a" not in write_mode:
name += f"_{datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S')}"
file_handler = logging.FileHandler(
os.path.join(log_folder, f"{name}.txt"),
write_mode,
@@ -467,7 +583,25 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
sys.excepthook = handle_exception
logging.info(f"Archipelago ({__version__}) logging initialized.")
def _cleanup():
for file in os.scandir(log_folder):
if file.name.endswith(".txt"):
last_change = datetime.datetime.fromtimestamp(file.stat().st_mtime)
if datetime.datetime.now() - last_change > datetime.timedelta(days=7):
try:
os.unlink(file.path)
except Exception as e:
logging.exception(e)
else:
logging.debug(f"Deleted old logfile {file.path}")
import threading
threading.Thread(target=_cleanup, name="LogCleaner").start()
import platform
logging.info(
f"Archipelago ({__version__}) logging initialized"
f" on {platform.platform()}"
f" running Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
)
def stream_input(stream, queue):
@@ -616,10 +750,96 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))):
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
def sorter(element: str) -> str:
def sorter(element: Union[str, Dict[str, Any]]) -> str:
if (not isinstance(element, str)):
element = element["title"]
parts = element.split(maxsplit=1)
if parts[0].lower() in ignore:
return parts[1].lower()
else:
return element.lower()
return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i))
def read_snes_rom(stream: BinaryIO, strip_header: bool = True) -> bytearray:
"""Reads rom into bytearray and optionally strips off any smc header"""
buffer = bytearray(stream.read())
if strip_header and len(buffer) % 0x400 == 0x200:
return buffer[0x200:]
return buffer
_faf_tasks: "Set[asyncio.Task[typing.Any]]" = set()
def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = None) -> None:
"""
Use this to start a task when you don't keep a reference to it or immediately await it,
to prevent early garbage collection. "fire-and-forget"
"""
# https://docs.python.org/3.10/library/asyncio-task.html#asyncio.create_task
# Python docs:
# ```
# Important: Save a reference to the result of [asyncio.create_task],
# to avoid a task disappearing mid-execution.
# ```
# This implementation follows the pattern given in that documentation.
task: asyncio.Task[typing.Any] = asyncio.create_task(co, name=name)
_faf_tasks.add(task)
task.add_done_callback(_faf_tasks.discard)
def deprecate(message: str):
if __debug__:
raise Exception(message)
import warnings
warnings.warn(message)
def _extend_freeze_support() -> None:
"""Extend multiprocessing.freeze_support() to also work on Non-Windows for spawn."""
# upstream issue: https://github.com/python/cpython/issues/76327
# code based on https://github.com/pyinstaller/pyinstaller/blob/develop/PyInstaller/hooks/rthooks/pyi_rth_multiprocessing.py#L26
import multiprocessing
import multiprocessing.spawn
def _freeze_support() -> None:
"""Minimal freeze_support. Only apply this if frozen."""
from subprocess import _args_from_interpreter_flags
# Prevent `spawn` from trying to read `__main__` in from the main script
multiprocessing.process.ORIGINAL_DIR = None
# Handle the first process that MP will create
if (
len(sys.argv) >= 2 and sys.argv[-2] == '-c' and sys.argv[-1].startswith((
'from multiprocessing.semaphore_tracker import main', # Py<3.8
'from multiprocessing.resource_tracker import main', # Py>=3.8
'from multiprocessing.forkserver import main'
)) and set(sys.argv[1:-2]) == set(_args_from_interpreter_flags())
):
exec(sys.argv[-1])
sys.exit()
# Handle the second process that MP will create
if multiprocessing.spawn.is_forking(sys.argv):
kwargs = {}
for arg in sys.argv[2:]:
name, value = arg.split('=')
if value == 'None':
kwargs[name] = None
else:
kwargs[name] = int(value)
multiprocessing.spawn.spawn_main(**kwargs)
sys.exit()
if not is_windows and is_frozen():
multiprocessing.freeze_support = multiprocessing.spawn.freeze_support = _freeze_support
def freeze_support() -> None:
"""This behaves like multiprocessing.freeze_support but also works on Non-Windows."""
import multiprocessing
_extend_freeze_support()
multiprocessing.freeze_support()

445
WargrooveClient.py Normal file
View File

@@ -0,0 +1,445 @@
from __future__ import annotations
import atexit
import os
import sys
import asyncio
import random
import shutil
from typing import Tuple, List, Iterable, Dict
from worlds.wargroove import WargrooveWorld
from worlds.wargroove.Items import item_table, faction_table, CommanderData, ItemData
import ModuleUpdate
ModuleUpdate.update()
import Utils
import json
import logging
if __name__ == "__main__":
Utils.init_logging("WargrooveClient", exception_logger="Client")
from NetUtils import NetworkItem, ClientStatus
from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \
CommonContext, server_loop
wg_logger = logging.getLogger("WG")
class WargrooveClientCommandProcessor(ClientCommandProcessor):
def _cmd_resync(self):
"""Manually trigger a resync."""
self.output(f"Syncing items.")
self.ctx.syncing = True
def _cmd_commander(self, *commander_name: Iterable[str]):
"""Set the current commander to the given commander."""
if commander_name:
self.ctx.set_commander(' '.join(commander_name))
else:
if self.ctx.can_choose_commander:
commanders = self.ctx.get_commanders()
wg_logger.info('Unlocked commanders: ' +
', '.join((commander.name for commander, unlocked in commanders if unlocked)))
wg_logger.info('Locked commanders: ' +
', '.join((commander.name for commander, unlocked in commanders if not unlocked)))
else:
wg_logger.error('Cannot set commanders in this game mode.')
class WargrooveContext(CommonContext):
command_processor: int = WargrooveClientCommandProcessor
game = "Wargroove"
items_handling = 0b111 # full remote
current_commander: CommanderData = faction_table["Starter"][0]
can_choose_commander: bool = False
commander_defense_boost_multiplier: int = 0
income_boost_multiplier: int = 0
starting_groove_multiplier: float
faction_item_ids = {
'Starter': 0,
'Cherrystone': 52025,
'Felheim': 52026,
'Floran': 52027,
'Heavensong': 52028,
'Requiem': 52029,
'Outlaw': 52030
}
buff_item_ids = {
'Income Boost': 52023,
'Commander Defense Boost': 52024,
}
def __init__(self, server_address, password):
super(WargrooveContext, self).__init__(server_address, password)
self.send_index: int = 0
self.syncing = False
self.awaiting_bridge = False
# self.game_communication_path: files go in this path to pass data between us and the actual game
if "appdata" in os.environ:
options = Utils.get_options()
root_directory = os.path.join(options["wargroove_options"]["root_directory"])
data_directory = os.path.join("lib", "worlds", "wargroove", "data")
dev_data_directory = os.path.join("worlds", "wargroove", "data")
appdata_wargroove = os.path.expandvars(os.path.join("%APPDATA%", "Chucklefish", "Wargroove"))
if not os.path.isfile(os.path.join(root_directory, "win64_bin", "wargroove64.exe")):
print_error_and_close("WargrooveClient couldn't find wargroove64.exe. "
"Unable to infer required game_communication_path")
self.game_communication_path = os.path.join(root_directory, "AP")
if not os.path.exists(self.game_communication_path):
os.makedirs(self.game_communication_path)
self.remove_communication_files()
atexit.register(self.remove_communication_files)
if not os.path.isdir(appdata_wargroove):
print_error_and_close("WargrooveClient couldn't find Wargoove in appdata!"
"Boot Wargroove and then close it to attempt to fix this error")
if not os.path.isdir(data_directory):
data_directory = dev_data_directory
if not os.path.isdir(data_directory):
print_error_and_close("WargrooveClient couldn't find Wargoove mod and save files in install!")
shutil.copytree(data_directory, appdata_wargroove, dirs_exist_ok=True)
else:
print_error_and_close("WargrooveClient couldn't detect system type. "
"Unable to infer required game_communication_path")
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(WargrooveContext, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
async def connection_closed(self):
await super(WargrooveContext, self).connection_closed()
self.remove_communication_files()
@property
def endpoints(self):
if self.server:
return [self.server]
else:
return []
async def shutdown(self):
await super(WargrooveContext, self).shutdown()
self.remove_communication_files()
def remove_communication_files(self):
for root, dirs, files in os.walk(self.game_communication_path):
for file in files:
os.remove(root + "/" + file)
def on_package(self, cmd: str, args: dict):
if cmd in {"Connected"}:
filename = f"AP_settings.json"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
slot_data = args["slot_data"]
json.dump(args["slot_data"], f)
self.can_choose_commander = slot_data["can_choose_commander"]
print('can choose commander:', self.can_choose_commander)
self.starting_groove_multiplier = slot_data["starting_groove_multiplier"]
self.income_boost_multiplier = slot_data["income_boost"]
self.commander_defense_boost_multiplier = slot_data["commander_defense_boost"]
f.close()
for ss in self.checked_locations:
filename = f"send{ss}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.close()
self.update_commander_data()
self.ui.update_tracker()
random.seed(self.seed_name + str(self.slot))
# Our indexes start at 1 and we have 24 levels
for i in range(1, 25):
filename = f"seed{i}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.write(str(random.randint(0, 4294967295)))
f.close()
if cmd in {"RoomInfo"}:
self.seed_name = args["seed_name"]
if cmd in {"ReceivedItems"}:
received_ids = [item.item for item in self.items_received]
for network_item in self.items_received:
filename = f"AP_{str(network_item.item)}.item"
path = os.path.join(self.game_communication_path, filename)
# Newly-obtained items
if not os.path.isfile(path):
open(path, 'w').close()
# Announcing commander unlocks
item_name = self.item_names[network_item.item]
if item_name in faction_table.keys():
for commander in faction_table[item_name]:
logger.info(f"{commander.name} has been unlocked!")
with open(path, 'w') as f:
item_count = received_ids.count(network_item.item)
if self.buff_item_ids["Income Boost"] == network_item.item:
f.write(f"{item_count * self.income_boost_multiplier}")
elif self.buff_item_ids["Commander Defense Boost"] == network_item.item:
f.write(f"{item_count * self.commander_defense_boost_multiplier}")
else:
f.write(f"{item_count}")
f.close()
print_filename = f"AP_{str(network_item.item)}.item.print"
print_path = os.path.join(self.game_communication_path, print_filename)
if not os.path.isfile(print_path):
open(print_path, 'w').close()
with open(print_path, 'w') as f:
f.write("Received " +
self.item_names[network_item.item] +
" from " +
self.player_names[network_item.player])
f.close()
self.update_commander_data()
self.ui.update_tracker()
if cmd in {"RoomUpdate"}:
if "checked_locations" in args:
for ss in self.checked_locations:
filename = f"send{ss}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.close()
def run_gui(self):
"""Import kivy UI system and start running it as self.ui_task."""
from kvui import GameManager, HoverBehavior, ServerToolTip
from kivy.uix.tabbedpanel import TabbedPanelItem
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.togglebutton import ToggleButton
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.image import AsyncImage, Image
from kivy.uix.stacklayout import StackLayout
from kivy.uix.label import Label
from kivy.properties import ColorProperty
from kivy.uix.image import Image
import pkgutil
class TrackerLayout(BoxLayout):
pass
class CommanderSelect(BoxLayout):
pass
class CommanderButton(ToggleButton):
pass
class FactionBox(BoxLayout):
pass
class CommanderGroup(BoxLayout):
pass
class ItemTracker(BoxLayout):
pass
class ItemLabel(Label):
pass
class WargrooveManager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
("WG", "WG Console"),
]
base_title = "Archipelago Wargroove Client"
ctx: WargrooveContext
unit_tracker: ItemTracker
trigger_tracker: BoxLayout
boost_tracker: BoxLayout
commander_buttons: Dict[int, List[CommanderButton]]
tracker_items = {
"Swordsman": ItemData(None, "Unit", False),
"Dog": ItemData(None, "Unit", False),
**item_table
}
def build(self):
container = super().build()
panel = TabbedPanelItem(text="Wargroove")
panel.content = self.build_tracker()
self.tabs.add_widget(panel)
return container
def build_tracker(self) -> TrackerLayout:
try:
tracker = TrackerLayout(orientation="horizontal")
commander_select = CommanderSelect(orientation="vertical")
self.commander_buttons = {}
for faction, commanders in faction_table.items():
faction_box = FactionBox(size_hint=(None, None), width=100 * len(commanders), height=70)
commander_group = CommanderGroup()
commander_buttons = []
for commander in commanders:
commander_button = CommanderButton(text=commander.name, group="commanders")
if faction == "Starter":
commander_button.disabled = False
commander_button.bind(on_press=lambda instance: self.ctx.set_commander(instance.text))
commander_buttons.append(commander_button)
commander_group.add_widget(commander_button)
self.commander_buttons[faction] = commander_buttons
faction_box.add_widget(Label(text=faction, size_hint_x=None, pos_hint={'left': 1}, size_hint_y=None, height=10))
faction_box.add_widget(commander_group)
commander_select.add_widget(faction_box)
item_tracker = ItemTracker(padding=[0,20])
self.unit_tracker = BoxLayout(orientation="vertical")
other_tracker = BoxLayout(orientation="vertical")
self.trigger_tracker = BoxLayout(orientation="vertical")
self.boost_tracker = BoxLayout(orientation="vertical")
other_tracker.add_widget(self.trigger_tracker)
other_tracker.add_widget(self.boost_tracker)
item_tracker.add_widget(self.unit_tracker)
item_tracker.add_widget(other_tracker)
tracker.add_widget(commander_select)
tracker.add_widget(item_tracker)
self.update_tracker()
return tracker
except Exception as e:
print(e)
def update_tracker(self):
received_ids = [item.item for item in self.ctx.items_received]
for faction, item_id in self.ctx.faction_item_ids.items():
for commander_button in self.commander_buttons[faction]:
commander_button.disabled = not (faction == "Starter" or item_id in received_ids)
self.unit_tracker.clear_widgets()
self.trigger_tracker.clear_widgets()
for name, item in self.tracker_items.items():
if item.type in ("Unit", "Trigger"):
status_color = (1, 1, 1, 1) if item.code is None or item.code in received_ids else (0.6, 0.2, 0.2, 1)
label = ItemLabel(text=name, color=status_color)
if item.type == "Unit":
self.unit_tracker.add_widget(label)
else:
self.trigger_tracker.add_widget(label)
self.boost_tracker.clear_widgets()
extra_income = received_ids.count(52023) * self.ctx.income_boost_multiplier
extra_defense = received_ids.count(52024) * self.ctx.commander_defense_boost_multiplier
income_boost = ItemLabel(text="Extra Income: " + str(extra_income))
defense_boost = ItemLabel(text="Comm Defense: " + str(100 + extra_defense))
self.boost_tracker.add_widget(income_boost)
self.boost_tracker.add_widget(defense_boost)
self.ui = WargrooveManager(self)
data = pkgutil.get_data(WargrooveWorld.__module__, "Wargroove.kv").decode()
Builder.load_string(data)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def update_commander_data(self):
if self.can_choose_commander:
faction_items = 0
faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()]
for network_item in self.items_received:
if self.item_names[network_item.item] in faction_item_names:
faction_items += 1
starting_groove = (faction_items - 1) * self.starting_groove_multiplier
# Must be an integer larger than 0
starting_groove = int(max(starting_groove, 0))
data = {
"commander": self.current_commander.internal_name,
"starting_groove": starting_groove
}
else:
data = {
"commander": "seed",
"starting_groove": 0
}
filename = 'commander.json'
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
json.dump(data, f)
if self.ui:
self.ui.update_tracker()
def set_commander(self, commander_name: str) -> bool:
"""Sets the current commander to the given one, if possible"""
if not self.can_choose_commander:
wg_logger.error("Cannot set commanders in this game mode.")
return
match_name = commander_name.lower()
for commander, unlocked in self.get_commanders():
if commander.name.lower() == match_name or commander.alt_name and commander.alt_name.lower() == match_name:
if unlocked:
self.current_commander = commander
self.syncing = True
wg_logger.info(f"Commander set to {commander.name}.")
self.update_commander_data()
return True
else:
wg_logger.error(f"Commander {commander.name} has not been unlocked.")
return False
else:
wg_logger.error(f"{commander_name} is not a recognized Wargroove commander.")
def get_commanders(self) -> List[Tuple[CommanderData, bool]]:
"""Gets a list of commanders with their unlocked status"""
commanders = []
received_ids = [item.item for item in self.items_received]
for faction in faction_table.keys():
unlocked = faction == 'Starter' or self.faction_item_ids[faction] in received_ids
commanders += [(commander, unlocked) for commander in faction_table[faction]]
return commanders
async def game_watcher(ctx: WargrooveContext):
from worlds.wargroove.Locations import location_table
while not ctx.exit_event.is_set():
if ctx.syncing == True:
sync_msg = [{'cmd': 'Sync'}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
ctx.syncing = False
sending = []
victory = False
for root, dirs, files in os.walk(ctx.game_communication_path):
for file in files:
if file.find("send") > -1:
st = file.split("send", -1)[1]
sending = sending+[(int(st))]
if file.find("victory") > -1:
victory = True
ctx.locations_checked = sending
message = [{"cmd": 'LocationChecks', "locations": sending}]
await ctx.send_msgs(message)
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
await asyncio.sleep(0.1)
def print_error_and_close(msg):
logger.error("Error: " + msg)
Utils.messagebox("Error", msg, error=True)
sys.exit(1)
if __name__ == '__main__':
async def main(args):
ctx = WargrooveContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
progression_watcher = asyncio.create_task(
game_watcher(ctx), name="WargrooveProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
await progression_watcher
await ctx.shutdown()
import colorama
parser = get_base_parser(description="Wargroove Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -1,5 +1,4 @@
import os
import sys
import multiprocessing
import logging
import typing
@@ -30,10 +29,15 @@ if not os.path.exists(configpath): # fall back to config.yaml in home
def get_app():
register()
app = raw_app
if os.path.exists(configpath):
if os.path.exists(configpath) and not app.config["TESTING"]:
import yaml
app.config.from_file(configpath, yaml.safe_load)
logging.info(f"Updated config from {configpath}")
if not app.config["HOST_ADDRESS"]:
logging.info("Getting public IP, as HOST_ADDRESS is empty.")
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}")
db.bind(**app.config["PONY"])
db.generate_mapping(create_tables=True)
return app

View File

@@ -1,16 +1,15 @@
import os
import uuid
import base64
import os
import socket
import uuid
from pony.flask import Pony
from flask import Flask
from flask_caching import Cache
from flask_compress import Compress
from pony.flask import Pony
from werkzeug.routing import BaseConverter
from Utils import title_sorted
from .models import *
UPLOAD_FOLDER = os.path.relpath('uploads')
LOGS_FOLDER = os.path.relpath('logs')
@@ -25,6 +24,8 @@ app.jinja_env.filters['all'] = all
app.config["SELFHOST"] = True # application process is in charge of running the websites
app.config["GENERATORS"] = 8 # maximum concurrent world gens
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
app.config["DEBUG"] = False
app.config["PORT"] = 80
@@ -32,8 +33,10 @@ app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 * 1024 # 64 megabyte limit
# if you want to deploy, make sure you have a non-guessable secret key
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
# at what amount of worlds should scheduling be used, instead of rolling in the webthread
app.config["JOB_THRESHOLD"] = 2
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
app.config["JOB_THRESHOLD"] = 1
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
app.config["JOB_TIME"] = 600
app.config['SESSION_PERMANENT'] = True
# waitress uses one thread for I/O, these are for processing of views that then get sent
@@ -48,7 +51,7 @@ app.config["PONY"] = {
app.config["MAX_ROLL"] = 20
app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache"
app.config["JSON_AS_ASCII"] = False
app.config["PATCH_TARGET"] = "archipelago.gg"
app.config["HOST_ADDRESS"] = ""
cache = Cache(app)
Compress(app)
@@ -73,8 +76,10 @@ def register():
"""Import submodules, triggering their registering on flask routing.
Note: initializes worlds subsystem."""
# has automatic patch integration
import Patch
app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types
import worlds.AutoWorld
import worlds.Files
app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: \
game_name in worlds.Files.AutoPatchRegister.patch_types
from WebHostLib.customserver import run_server_process
# to trigger app routing picking up on it

View File

@@ -1,11 +1,11 @@
"""API endpoints package."""
from uuid import UUID
from typing import List, Tuple
from uuid import UUID
from flask import Blueprint, abort
from ..models import Room, Seed
from .. import cache
from ..models import Room, Seed
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
@@ -40,10 +40,20 @@ def get_datapackage():
@api_endpoints.route('/datapackage_version')
@cache.cached()
def get_datapackage_versions():
from worlds import network_data_package, AutoWorldRegister
from worlds import AutoWorldRegister
version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()}
version_package["version"] = network_data_package["version"]
return version_package
from . import generate, user, tracker # trigger registration
@api_endpoints.route('/datapackage_checksum')
@cache.cached()
def get_datapackage_checksums():
from worlds import network_data_package
version_package = {
game: game_data["checksum"] for game, game_data in network_data_package["games"].items()
}
return version_package
from . import generate, user # trigger registration

View File

@@ -1,15 +1,16 @@
import json
import pickle
from uuid import UUID
from . import api_endpoints
from flask import request, session, url_for
from markupsafe import Markup
from pony.orm import commit
from WebHostLib import app, Generation, STATE_QUEUED, Seed, STATE_ERROR
from WebHostLib import app
from WebHostLib.check import get_yaml_data, roll_options
from WebHostLib.generate import get_meta
from WebHostLib.models import Generation, STATE_QUEUED, Seed, STATE_ERROR
from . import api_endpoints
@api_endpoints.route('/generate', methods=['POST'])
@@ -21,13 +22,18 @@ def generate_api():
if 'file' in request.files:
file = request.files['file']
options = get_yaml_data(file)
if type(options) == str:
if isinstance(options, Markup):
return {"text": options.striptags()}, 400
if isinstance(options, str):
return {"text": options}, 400
if "race" in request.form:
race = bool(0 if request.form["race"] in {"false"} else int(request.form["race"]))
meta_options_source = request.form
json_data = request.get_json()
# json_data is optional, we can have it silently fall to None as it used to do.
# See https://flask.palletsprojects.com/en/2.2.x/api/#flask.Request.get_json -> Changelog -> 2.1
json_data = request.get_json(silent=True)
if json_data:
meta_options_source = json_data
if 'weights' in json_data:
@@ -43,9 +49,8 @@ def generate_api():
if len(options) > app.config["MAX_ROLL"]:
return {"text": "Max size of multiworld exceeded",
"detail": app.config["MAX_ROLL"]}, 409
meta = get_meta(meta_options_source)
meta["race"] = race
results, gen_options = roll_options(options, meta["plando_options"])
meta = get_meta(meta_options_source, race)
results, gen_options = roll_options(options, set(meta["plando_options"]))
if any(type(result) == str for result in results.values()):
return {"text": str(results),
"detail": results}, 400

View File

@@ -1,50 +0,0 @@
import collections
from flask import jsonify
from typing import Optional, Dict, Any, Tuple, List
from Utils import restricted_loads
from uuid import UUID
from ..models import Room
from . import api_endpoints
from ..tracker import fill_tracker_data, get_static_room_data
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
from WebHostLib import cache
@api_endpoints.route('/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
@cache.memoize(timeout=60)
def update_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int):
room: Optional[Room] = Room.get(tracker=tracker)
locations = get_static_room_data(room)[0]
items_counter: Dict[int, collections.Counter] = get_item_names_counter(locations)
player_tracker, multisave, inventory, seed_checks_in_area, lttp_checks_done, \
slot_data, games, player_name, display_icons = fill_tracker_data(room, tracked_team, tracked_player)
# convert numbers to string
for item in player_tracker.items_received:
if items_counter[tracked_player][item] == 1:
player_tracker.items_received[item] = ''
else:
player_tracker.items_received[item] = str(player_tracker.items_received[item])
return jsonify({
"items_received": player_tracker.items_received,
"checked_locations": list(sorted(player_tracker.checked_locations)),
"icons": display_icons,
"progressive_names": player_tracker.progressive_names
})
@cache.cached()
def get_item_names_counter(locations: Dict[int, Dict[int, Tuple[int, int, int]]]):
# create and fill dictionary of all progression items for players
items_counters: Dict[int, collections.Counter] = {}
for player in locations:
for location in locations[player]:
item, recipient, flags = locations[player][location]
item_name = lookup_any_item_id_to_name[item]
items_counters.setdefault(recipient, collections.Counter())[item_name] += 1
return items_counters

View File

@@ -1,6 +1,7 @@
from flask import session, jsonify
from pony.orm import select
from WebHostLib.models import *
from WebHostLib.models import Room, Seed
from . import api_endpoints, get_players

View File

@@ -1,14 +1,14 @@
from __future__ import annotations
import logging
import json
import multiprocessing
import threading
from datetime import timedelta, datetime
import sys
import typing
import time
import json
import logging
import multiprocessing
import os
import sys
import threading
import time
import typing
from datetime import timedelta, datetime
from pony.orm import db_session, select, commit
@@ -135,7 +135,7 @@ def autogen(config: dict):
with Locker("autogen"):
with multiprocessing.Pool(config["GENERATORS"], initializer=init_db,
initargs=(config["PONY"],)) as generator_pool:
initargs=(config["PONY"],), maxtasksperchild=10) as generator_pool:
with db_session:
to_start = select(generation for generation in Generation if generation.state == STATE_STARTED)
@@ -177,6 +177,9 @@ class MultiworldInstance():
with guardian_lock:
multiworlds[self.room_id] = self
self.ponyconfig = config["PONY"]
self.cert = config["SELFLAUNCHCERT"]
self.key = config["SELFLAUNCHKEY"]
self.host = config["HOST_ADDRESS"]
def start(self):
if self.process and self.process.is_alive():
@@ -184,7 +187,8 @@ class MultiworldInstance():
logging.info(f"Spinning up {self.room_id}")
process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.room_id, self.ponyconfig, get_static_server_data()),
args=(self.room_id, self.ponyconfig, get_static_server_data(),
self.cert, self.key, self.host),
name="MultiHost")
process.start()
# bind after start to prevent thread sync issues with guardian.

View File

@@ -1,7 +1,8 @@
import zipfile
from typing import *
from flask import request, flash, redirect, url_for, session, render_template
from flask import request, flash, redirect, url_for, render_template
from markupsafe import Markup
from WebHostLib import app
@@ -12,7 +13,7 @@ def allowed_file(filename):
return filename.endswith(('.txt', ".yaml", ".zip"))
from Generate import roll_settings, PlandoSettings
from Generate import roll_settings, PlandoOptions
from Utils import parse_yamls
@@ -25,7 +26,7 @@ def check():
else:
file = request.files['file']
options = get_yaml_data(file)
if type(options) == str:
if isinstance(options, str):
flash(options)
else:
results, _ = roll_options(options)
@@ -38,7 +39,7 @@ def mysterycheck():
return redirect(url_for("check"), 301)
def get_yaml_data(file) -> Union[Dict[str, str], str]:
def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]:
options = {}
# if user does not select file, browser also
# submit an empty part without filename
@@ -50,9 +51,14 @@ def get_yaml_data(file) -> Union[Dict[str, str], str]:
with zipfile.ZipFile(file, 'r') as zfile:
infolist = zfile.infolist()
if any(file.filename.endswith(".archipelago") for file in infolist):
return Markup("Error: Your .zip file contains an .archipelago file. "
'Did you mean to <a href="/uploads">host a game</a>?')
for file in infolist:
if file.filename.endswith(banned_zip_contents):
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
"Your file was deleted."
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
options[file.filename] = zfile.open(file, "r").read()
else:
@@ -65,7 +71,7 @@ def get_yaml_data(file) -> Union[Dict[str, str], str]:
def roll_options(options: Dict[str, Union[dict, str]],
plando_options: Set[str] = frozenset({"bosses", "items", "connections", "texts"})) -> \
Tuple[Dict[str, Union[str, bool]], Dict[str, dict]]:
plando_options = PlandoSettings.from_set(set(plando_options))
plando_options = PlandoOptions.from_set(set(plando_options))
results = {}
rolled_results = {}
for filename, text in options.items():
@@ -86,7 +92,7 @@ def roll_options(options: Dict[str, Union[dict, str]],
rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data,
plando_options=plando_options)
except Exception as e:
results[filename] = f"Failed to generate mystery in {filename}: {e}"
results[filename] = f"Failed to generate options in {filename}: {e}"
else:
results[filename] = True
return results, rolled_results

View File

@@ -10,13 +10,16 @@ import random
import socket
import threading
import time
import typing
import websockets
from pony.orm import commit, db_session, select
import Utils
from .models import db_session, Room, select, commit, Command, db
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
from Utils import restricted_loads, cache_argsless
from .models import Command, GameDataPackage, Room, db
class CustomClientMessageProcessor(ClientMessageProcessor):
@@ -65,7 +68,6 @@ class WebHostContext(Context):
def _load_game_data(self):
for key, value in self.static_server_data.items():
setattr(self, key, value)
self.forced_auto_forfeits = collections.defaultdict(lambda: False, self.forced_auto_forfeits)
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
def listen_to_db_commands(self):
@@ -90,7 +92,21 @@ class WebHostContext(Context):
else:
self.port = get_random_port()
return self._load(self.decompress(room.seed.multidata), True)
multidata = self.decompress(room.seed.multidata)
game_data_packages = {}
for game in list(multidata.get("datapackage", {})):
game_data = multidata["datapackage"][game]
if "checksum" in game_data:
if self.gamespackage.get(game, {}).get("checksum") == game_data["checksum"]:
# non-custom. remove from multidata
# games package could be dropped from static data once all rooms embed data package
del multidata["datapackage"][game]
else:
row = GameDataPackage.get(checksum=game_data["checksum"])
if row: # None if rolled on >= 0.3.9 but uploaded to <= 0.3.8. multidata should be complete
game_data_packages[game] = Utils.restricted_loads(row.data)
return self._load(multidata, game_data_packages, True)
@db_session
def init_save(self, enabled: bool = True):
@@ -125,21 +141,23 @@ def get_random_port():
def get_static_server_data() -> dict:
import worlds
data = {
"forced_auto_forfeits": {},
"non_hintable_names": {},
"gamespackage": worlds.network_data_package["games"],
"item_name_groups": {world_name: world.item_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()},
"location_name_groups": {world_name: world.location_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()},
}
for world_name, world in worlds.AutoWorldRegister.world_types.items():
data["forced_auto_forfeits"][world_name] = world.forced_auto_forfeit
data["non_hintable_names"][world_name] = world.hint_blacklist
return data
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict):
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
host: str):
# establish DB connection for multidata and multisave
db.bind(**ponyconfig)
db.generate_mapping(check_tables=False)
@@ -149,32 +167,31 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict):
ctx = WebHostContext(static_server_data)
ctx.load(room_id)
ctx.init_save()
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
try:
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None,
ping_interval=None)
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
await ctx.server
except Exception: # likely port in use - in windows this is OSError, but I didn't check the others
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ping_timeout=None,
ping_interval=None)
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
await ctx.server
port = 0
for wssocket in ctx.server.ws_server.sockets:
socketname = wssocket.getsockname()
if wssocket.family == socket.AF_INET6:
logging.info(f'Hosting game at [{get_public_ipv6()}]:{socketname[1]}')
# Prefer IPv4, as most users seem to not have working ipv6 support
if not port:
port = socketname[1]
elif wssocket.family == socket.AF_INET:
logging.info(f'Hosting game at {get_public_ipv4()}:{socketname[1]}')
port = socketname[1]
if port:
logging.info(f'Hosting game at {host}:{port}')
with db_session:
room = Room.get(id=ctx.room_id)
room.last_port = port
else:
logging.exception("Could not determine port. Likely hosting failure.")
with db_session:
ctx.auto_shutdown = Room.get(id=room_id).timeout
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
@@ -183,4 +200,17 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict):
from .autolauncher import Locker
with Locker(room_id):
asyncio.run(main())
try:
asyncio.run(main())
except KeyboardInterrupt:
with db_session:
room = Room.get(id=room_id)
# ensure the Room does not spin up again on its own, minute of safety buffer
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
except:
with db_session:
room = Room.get(id=room_id)
room.last_port = -1
# ensure the Room does not spin up again on its own, minute of safety buffer
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
raise

View File

@@ -1,12 +1,13 @@
import zipfile
import json
import zipfile
from io import BytesIO
from flask import send_file, Response, render_template
from pony.orm import select
from Patch import update_patch_data, preferred_endings, AutoPatchRegister
from WebHostLib import app, Slot, Room, Seed, cache
from worlds.Files import AutoPatchRegister
from . import app, cache
from .models import Slot, Room, Seed
@app.route("/dl_patch/<suuid:room_id>/<int:patch_id>")
@@ -25,7 +26,7 @@ def download_patch(room_id, patch_id):
with zipfile.ZipFile(filelike, "a") as zf:
with zf.open("archipelago.json", "r") as f:
manifest = json.load(f)
manifest["server"] = f"{app.config['PATCH_TARGET']}:{last_port}" if last_port else None
manifest["server"] = f"{app.config['HOST_ADDRESS']}:{last_port}" if last_port else None
with zipfile.ZipFile(new_file, "w") as new_zip:
for file in zf.infolist():
if file.filename == "archipelago.json":
@@ -41,12 +42,7 @@ def download_patch(room_id, patch_id):
new_file.seek(0)
return send_file(new_file, as_attachment=True, download_name=fname)
else:
patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}")
patch_data = BytesIO(patch_data)
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \
f"{preferred_endings[patch.game]}"
return send_file(patch_data, as_attachment=True, download_name=fname)
return "Old Patch file, no longer compatible."
@app.route("/dl_spoiler/<suuid:seed_id>")
@@ -68,7 +64,7 @@ def download_slot_file(room_id, player_id: int):
if slot_data.game == "Minecraft":
from worlds.minecraft import mc_update_output
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port)
data = mc_update_output(slot_data.data, server=app.config['HOST_ADDRESS'], port=room.last_port)
return send_file(io.BytesIO(data), as_attachment=True, download_name=fname)
elif slot_data.game == "Factorio":
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
@@ -76,13 +72,24 @@ def download_slot_file(room_id, player_id: int):
if name.endswith("info.json"):
fname = name.rsplit("/", 1)[0] + ".zip"
elif slot_data.game == "Ocarina of Time":
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apz5"
stream = io.BytesIO(slot_data.data)
if zipfile.is_zipfile(stream):
with zipfile.ZipFile(stream) as zf:
for name in zf.namelist():
if name.endswith(".zpf"):
fname = name.rsplit(".", 1)[0] + ".apz5"
else: # pre-ootr-7.0 support
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apz5"
elif slot_data.game == "VVVVVV":
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apv6"
elif slot_data.game == "Zillion":
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apzl"
elif slot_data.game == "Super Mario 64":
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apsm64ex"
elif slot_data.game == "Dark Souls III":
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json"
elif slot_data.game == "Kingdom Hearts 2":
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.zip"
else:
return "Game download not supported."
return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname)

View File

@@ -1,27 +1,28 @@
import os
import tempfile
import random
import json
import os
import pickle
import random
import tempfile
import zipfile
import concurrent.futures
from collections import Counter
from typing import Dict, Optional, Any
from Utils import __version__
from typing import Dict, Optional, Any, Union, List
from flask import request, flash, redirect, url_for, session, render_template
from pony.orm import commit, db_session
from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain
from BaseClasses import seeddigits, get_seed
from Generate import handle_name, PlandoSettings
import pickle
from .models import Generation, STATE_ERROR, STATE_QUEUED, commit, db_session, Seed, UUID
from Generate import handle_name, PlandoOptions
from Main import main as ERmain
from Utils import __version__
from WebHostLib import app
from worlds.alttp.EntranceRandomizer import parse_arguments
from .check import get_yaml_data, roll_options
from .models import Generation, STATE_ERROR, STATE_QUEUED, Seed, UUID
from .upload import upload_zip_to_db
def get_meta(options_source: dict) -> dict:
def get_meta(options_source: dict, race: bool = False) -> Dict[str, Union[List[str], Dict[str, Any]]]:
plando_options = {
options_source.get("plando_bosses", ""),
options_source.get("plando_items", ""),
@@ -32,13 +33,27 @@ def get_meta(options_source: dict) -> dict:
server_options = {
"hint_cost": int(options_source.get("hint_cost", 10)),
"forfeit_mode": options_source.get("forfeit_mode", "goal"),
"release_mode": options_source.get("release_mode", "goal"),
"remaining_mode": options_source.get("remaining_mode", "disabled"),
"collect_mode": options_source.get("collect_mode", "disabled"),
"item_cheat": bool(int(options_source.get("item_cheat", 1))),
"server_password": options_source.get("server_password", None),
}
return {"server_options": server_options, "plando_options": list(plando_options)}
generator_options = {
"spoiler": int(options_source.get("spoiler", 0)),
"race": race
}
if race:
server_options["item_cheat"] = False
server_options["remaining_mode"] = "disabled"
generator_options["spoiler"] = 0
return {
"server_options": server_options,
"plando_options": list(plando_options),
"generator_options": generator_options,
}
@app.route('/generate', methods=['GET', 'POST'])
@@ -51,16 +66,11 @@ def generate(race=False):
else:
file = request.files['file']
options = get_yaml_data(file)
if type(options) == str:
if isinstance(options, str):
flash(options)
else:
meta = get_meta(request.form)
meta["race"] = race
results, gen_options = roll_options(options, meta["plando_options"])
if race:
meta["server_options"]["item_cheat"] = False
meta["server_options"]["remaining_mode"] = "disabled"
meta = get_meta(request.form, race)
results, gen_options = roll_options(options, set(meta["plando_options"]))
if any(type(result) == str for result in results.values()):
return render_template("checkResult.html", results=results)
@@ -91,14 +101,14 @@ def generate(race=False):
return render_template("generate.html", race=race, version=__version__)
def gen_game(gen_options, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None):
def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None):
if not meta:
meta: Dict[str, Any] = {}
meta.setdefault("server_options", {}).setdefault("hint_cost", 10)
race = meta.setdefault("race", False)
race = meta.setdefault("generator_options", {}).setdefault("race", False)
try:
def task():
target = tempfile.TemporaryDirectory()
playercount = len(gen_options)
seed = get_seed()
@@ -113,13 +123,13 @@ def gen_game(gen_options, meta: Optional[Dict[str, Any]] = None, owner=None, sid
erargs = parse_arguments(['--multi', str(playercount)])
erargs.seed = seed
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwritten in mystery
erargs.spoiler = 0 if race else 2
erargs.spoiler = meta["generator_options"].get("spoiler", 0)
erargs.race = race
erargs.outputname = seedname
erargs.outputpath = target.name
erargs.teams = 1
erargs.plando_options = PlandoSettings.from_set(meta.setdefault("plando_options",
{"bosses", "items", "connections", "texts"}))
erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options",
{"bosses", "items", "connections", "texts"}))
name_counter = Counter()
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
@@ -138,6 +148,23 @@ def gen_game(gen_options, meta: Optional[Dict[str, Any]] = None, owner=None, sid
ERmain(erargs, seed, baked_server_options=meta["server_options"])
return upload_to_db(target.name, sid, owner, race)
thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
thread = thread_pool.submit(task)
try:
return thread.result(app.config["JOB_TIME"])
except concurrent.futures.TimeoutError as e:
if sid:
with db_session:
gen = Generation.get(id=sid)
if gen is not None:
gen.state = STATE_ERROR
meta = json.loads(gen.meta)
meta["error"] = (
"Allowed time for Generation exceeded, please consider generating locally instead. " +
e.__class__.__name__ + ": " + str(e))
gen.meta = json.dumps(meta)
commit()
except BaseException as e:
if sid:
with db_session:

View File

@@ -1,7 +1,11 @@
from datetime import timedelta, datetime
from flask import render_template
from pony.orm import count
from WebHostLib import app, cache
from .models import *
from datetime import timedelta
from .models import Room, Seed
@app.route('/', methods=['GET', 'POST'])
@cache.cached(timeout=300) # cache has to appear under app route for caching to work

View File

@@ -32,7 +32,7 @@ def update_sprites_lttp():
spriteData = []
for file in os.listdir(input_dir):
for file in (file for file in os.listdir(input_dir) if not file.startswith(".")):
sprite = Sprite(os.path.join(input_dir, file))
if not sprite.name:

View File

@@ -1,12 +1,14 @@
import datetime
import os
from typing import List, Dict, Union
import jinja2.exceptions
from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory
from pony.orm import count, commit, db_session
from .models import count, Seed, commit, Room, db_session, Command, UUID, uuid4
from worlds.AutoWorld import AutoWorldRegister
from . import app, cache
from .models import Seed, Room, Command, UUID, uuid4
def get_world_theme(game_name: str):
@@ -68,10 +70,6 @@ def tutorial(game, file, lang):
@app.route('/tutorial/')
def tutorial_landing():
worlds = {}
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
worlds[game] = world
return render_template("tutorialLanding.html")
@@ -118,7 +116,11 @@ def display_log(room: UUID):
if room is None:
return abort(404)
if room.owner == session["_id"]:
return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8")
file_path = os.path.join("logs", str(room.id) + ".txt")
if os.path.exists(file_path):
return Response(_read_log(file_path), mimetype="text/plain;charset=UTF-8")
return "Log File does not exist."
return "Access Denied", 403
@@ -151,7 +153,7 @@ def favicon():
@app.route('/discord')
def discord():
return redirect("https://discord.gg/archipelago")
return redirect("https://discord.gg/8Z65BR2")
@app.route('/datapackage')
@@ -166,8 +168,9 @@ def get_datapackage():
@app.route('/index')
@app.route('/sitemap')
def get_sitemap():
available_games = []
available_games: List[Dict[str, Union[str, bool]]] = []
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
available_games.append(game)
has_settings: bool = isinstance(world.web.settings_page, bool) and world.web.settings_page
available_games.append({ 'title': game, 'has_settings': has_settings })
return render_template("siteMap.html", games=available_games)

View File

@@ -1,6 +1,6 @@
from datetime import datetime
from uuid import UUID, uuid4
from pony.orm import *
from pony.orm import Database, PrimaryKey, Required, Set, Optional, buffer, LongStr
db = Database()
@@ -21,7 +21,7 @@ class Slot(db.Entity):
class Room(db.Entity):
id = PrimaryKey(UUID, default=uuid4)
last_activity = Required(datetime, default=lambda: datetime.utcnow(), index=True)
creation_time = Required(datetime, default=lambda: datetime.utcnow())
creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page
owner = Required(UUID, index=True)
commands = Set('Command')
seed = Required('Seed', index=True)
@@ -29,6 +29,7 @@ class Room(db.Entity):
show_spoiler = Required(int, default=0) # 0 -> never, 1 -> after completion, -> 2 always
timeout = Required(int, default=lambda: 2 * 60 * 60) # seconds since last activity to shutdown
tracker = Optional(UUID, index=True)
# Port special value -1 means the server errored out. Another attempt can be made with a page refresh
last_port = Optional(int, default=lambda: 0)
@@ -37,7 +38,7 @@ class Seed(db.Entity):
rooms = Set(Room)
multidata = Required(bytes, lazy=True)
owner = Required(UUID, index=True)
creation_time = Required(datetime, default=lambda: datetime.utcnow())
creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page
slots = Set(Slot)
spoiler = Optional(LongStr, lazy=True)
meta = Required(LongStr, default=lambda: "{\"race\": false}") # additional meta information/tags
@@ -55,3 +56,8 @@ class Generation(db.Entity):
options = Required(buffer, lazy=True)
meta = Required(LongStr, default=lambda: "{\"race\": false}")
state = Required(int, default=0, index=True)
class GameDataPackage(db.Entity):
checksum = PrimaryKey(str)
data = Required(bytes)

View File

@@ -1,52 +1,24 @@
import json
import logging
import os
from Utils import __version__, local_path
from jinja2 import Template
import yaml
import json
import typing
from worlds.AutoWorld import AutoWorldRegister
import yaml
from jinja2 import Template
import Options
from Utils import __version__, local_path
from worlds.AutoWorld import AutoWorldRegister
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
"exclude_locations"}
"exclude_locations", "priority_locations"}
def create():
target_folder = local_path("WebHostLib", "static", "generated")
os.makedirs(os.path.join(target_folder, "configs"), exist_ok=True)
yaml_folder = os.path.join(target_folder, "configs")
def dictify_range(option: typing.Union[Options.Range, Options.SpecialRange]):
data = {}
special = getattr(option, "special_range_cutoff", None)
if special is not None:
data[special] = 0
data.update({
option.range_start: 0,
option.range_end: 0,
"random": 0, "random-low": 0, "random-high": 0,
option.default: 50
})
notes = {
special: "minimum value without special meaning",
option.range_start: "minimum value",
option.range_end: "maximum value"
}
for name, number in getattr(option, "special_range_names", {}).items():
if number in data:
data[name] = data[number]
del data[number]
else:
data[name] = 0
return data, notes
def default_converter(default_value):
if isinstance(default_value, (set, frozenset)):
return list(default_value)
return default_value
Options.generate_yaml_templates(yaml_folder)
def get_html_doc(option_type: type(Options.Option)) -> str:
if not option_type.__doc__:
@@ -64,24 +36,15 @@ def create():
for game_name, world in AutoWorldRegister.world_types.items():
all_options = {**Options.per_game_common_options, **world.option_definitions}
with open(local_path("WebHostLib", "templates", "options.yaml")) as f:
file_data = f.read()
res = Template(file_data).render(
options=all_options,
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
dictify_range=dictify_range, default_converter=default_converter,
)
del file_data
with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f:
f.write(res)
all_options: typing.Dict[str, Options.AssembleOptions] = {
**Options.per_game_common_options,
**world.option_definitions
}
# Generate JSON files for player-settings pages
player_settings = {
"baseOptions": {
"description": "Generated by https://archipelago.gg/",
"description": f"Generated by https://archipelago.gg/ for {game_name}",
"game": game_name,
"name": "Player",
},
@@ -92,7 +55,7 @@ def create():
if option_name in handled_in_js:
pass
elif option.options:
elif issubclass(option, Options.Choice) or issubclass(option, Options.Toggle):
game_options[option_name] = this_option = {
"type": "select",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
@@ -102,20 +65,15 @@ def create():
}
for sub_option_id, sub_option_name in option.name_lookup.items():
this_option["options"].append({
"name": option.get_option_name(sub_option_id),
"value": sub_option_name,
})
if sub_option_name != "random":
this_option["options"].append({
"name": option.get_option_name(sub_option_id),
"value": sub_option_name,
})
if sub_option_id == option.default:
this_option["defaultValue"] = sub_option_name
this_option["options"].append({
"name": "Random",
"value": "random",
})
if option.default == "random":
if not this_option["defaultValue"]:
this_option["defaultValue"] = "random"
elif issubclass(option, Options.Range):
@@ -135,27 +93,30 @@ def create():
for key, val in option.special_range_names.items():
game_options[option_name]["value_names"][key] = val
elif getattr(option, "verify_item_name", False):
elif issubclass(option, Options.ItemSet):
game_options[option_name] = {
"type": "items-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
"defaultValue": list(option.default)
}
elif getattr(option, "verify_location_name", False):
elif issubclass(option, Options.LocationSet):
game_options[option_name] = {
"type": "locations-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
"defaultValue": list(option.default)
}
elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet):
elif issubclass(option, Options.VerifyKeys) and not issubclass(option, Options.OptionDict):
if option.valid_keys:
game_options[option_name] = {
"type": "custom-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
"options": list(option.valid_keys),
"defaultValue": list(option.default) if hasattr(option, "default") else []
}
else:
@@ -169,6 +130,14 @@ def create():
json.dump(player_settings, f, indent=2, separators=(',', ': '))
if not world.hidden and world.web.settings_page is True:
# Add the random option to Choice, TextChoice, and Toggle settings
for option in game_options.values():
if option["type"] == "select":
option["options"].append({"name": "Random", "value": "random"})
if not option["defaultValue"]:
option["defaultValue"] = "random"
weighted_settings["baseOptions"]["game"][game_name] = 0
weighted_settings["games"][game_name] = {}
weighted_settings["games"][game_name]["gameSettings"] = game_options

View File

@@ -1,7 +1,9 @@
flask>=2.2.2
pony>=0.7.16
flask>=2.2.3
pony>=0.7.16; python_version <= '3.10'
pony @ https://github.com/Berserker66/pony/releases/download/v0.7.16/pony-0.7.16-py3-none-any.whl#0.7.16 ; python_version >= '3.11'
waitress>=2.1.2
Flask-Caching>=2.0.1
Flask-Compress>=1.12
Flask-Limiter>=2.6.2
bokeh>=2.4.3
Flask-Caching>=2.0.2
Flask-Compress>=1.13
Flask-Limiter>=3.3.0
bokeh>=3.1.1
markupsafe>=2.1.3

View File

@@ -4,6 +4,7 @@ window.addEventListener('load', () => {
"ordering": true,
"info": false,
"dom": "t",
"stateSave": true,
});
console.log(tables);
});

View File

@@ -0,0 +1,40 @@
window.addEventListener('load', () => {
// Mobile menu handling
const menuButton = document.getElementById('base-header-mobile-menu-button');
const mobileMenu = document.getElementById('base-header-mobile-menu');
menuButton.addEventListener('click', (evt) => {
evt.preventDefault();
evt.stopPropagation();
if (!mobileMenu.style.display || mobileMenu.style.display === 'none') {
return mobileMenu.style.display = 'flex';
}
mobileMenu.style.display = 'none';
});
window.addEventListener('resize', () => {
mobileMenu.style.display = 'none';
});
// Popover handling
const popoverText = document.getElementById('base-header-popover-text');
const popoverMenu = document.getElementById('base-header-popover-menu');
popoverText.addEventListener('click', (evt) => {
evt.preventDefault();
evt.stopPropagation();
if (!popoverMenu.style.display || popoverMenu.style.display === 'none') {
return popoverMenu.style.display = 'flex';
}
popoverMenu.style.display = 'none';
});
document.body.addEventListener('click', () => {
mobileMenu.style.display = 'none';
popoverMenu.style.display = 'none';
});
});

View File

@@ -0,0 +1,49 @@
window.addEventListener('load', () => {
// Reload tracker every 60 seconds
const url = window.location;
setInterval(() => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
// Create a fake DOM using the returned HTML
const domParser = new DOMParser();
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
// Update item tracker
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
// Update only counters in the location-table
let counters = document.getElementsByClassName('counter');
const fakeCounters = fakeDOM.getElementsByClassName('counter');
for (let i = 0; i < counters.length; i++) {
counters[i].innerHTML = fakeCounters[i].innerHTML;
}
};
ajax.open('GET', url);
ajax.send();
}, 60000)
// Collapsible advancement sections
const categories = document.getElementsByClassName("location-category");
for (let i = 0; i < categories.length; i++) {
let hide_id = categories[i].id.split('-')[0];
if (hide_id == 'Total') {
continue;
}
categories[i].addEventListener('click', function() {
// Toggle the advancement list
document.getElementById(hide_id).classList.toggle("hide");
// Change text of the header
const tab_header = document.getElementById(hide_id+'-header').children[0];
const orig_text = tab_header.innerHTML;
let new_text;
if (orig_text.includes("▼")) {
new_text = orig_text.replace("▼", "▲");
}
else {
new_text = orig_text.replace("▲", "▼");
}
tab_header.innerHTML = new_text;
});
}
});

View File

@@ -26,24 +26,22 @@ window.addEventListener('load', () => {
adjustHeaderWidth();
// Reset the id of all header divs to something nicer
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/);
for (let i=0; i < headers.length; i++){
const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase()
headers[i].setAttribute('id', headerId);
headers[i].addEventListener('click', () =>
window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`);
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase();
header.setAttribute('id', headerId);
header.addEventListener('click', () => {
window.location.hash = `#${headerId}`;
header.scrollIntoView();
});
}
// Manually scroll the user to the appropriate header if anchor navigation is used
if (scrollTargetIndex > -1) {
try{
const scrollTarget = window.location.href.substring(scrollTargetIndex + 1);
document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" });
} catch(error) {
console.error(error);
document.fonts.ready.finally(() => {
if (window.location.hash) {
const scrollTarget = document.getElementById(window.location.hash.substring(1));
scrollTarget?.scrollIntoView();
}
}
});
}).catch((error) => {
console.error(error);
tutorialWrapper.innerHTML =

View File

@@ -20,7 +20,7 @@ comfortable exploiting certain glitches in the game.
## What is a multi-world?
While a randomizer shuffles a game, a multi-world randomizer shuffles that game for multiple players. For example, in a
two player multi-world, players A and B each get their own randomized version of a game, called seeds. In each player's
two player multi-world, players A and B each get their own randomized version of a game, called a world. In each player's
game, they may find items which belong to the other player. If player A finds an item which belongs to player B, the
item will be sent to player B's world over the internet.
@@ -29,7 +29,7 @@ their game.
## What happens if a person has to leave early?
If a player must leave early, they can use Archipelago's forfeit system. When a player forfeits their game, all the
If a player must leave early, they can use Archipelago's release system. When a player releases their game, all the
items in that game which belong to other players are sent out automatically, so other players can continue to play.
## What does multi-game mean?
@@ -46,7 +46,7 @@ the website is not required to generate them.
## How do I get started?
If you are ready to start randomizing games, or want to start playing your favorite randomizer with others, please join
our discord server at the [Archipelago Discord](https://discord.gg/archipelago). There are always people ready to answer
our discord server at the [Archipelago Discord](https://discord.gg/8Z65BR2). There are always people ready to answer
any questions you might have.
## What are some common terms I should know?

View File

@@ -26,24 +26,22 @@ window.addEventListener('load', () => {
adjustHeaderWidth();
// Reset the id of all header divs to something nicer
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/);
for (let i=0; i < headers.length; i++){
const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase()
headers[i].setAttribute('id', headerId);
headers[i].addEventListener('click', () =>
window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`);
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase();
header.setAttribute('id', headerId);
header.addEventListener('click', () => {
window.location.hash = `#${headerId}`;
header.scrollIntoView();
});
}
// Manually scroll the user to the appropriate header if anchor navigation is used
if (scrollTargetIndex > -1) {
try{
const scrollTarget = window.location.href.substring(scrollTargetIndex + 1);
document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" });
} catch(error) {
console.error(error);
document.fonts.ready.finally(() => {
if (window.location.hash) {
const scrollTarget = document.getElementById(window.location.hash.substring(1));
scrollTarget?.scrollIntoView();
}
}
});
}).catch((error) => {
console.error(error);
gameInfo.innerHTML =

View File

@@ -26,24 +26,22 @@ window.addEventListener('load', () => {
adjustHeaderWidth();
// Reset the id of all header divs to something nicer
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/);
for (let i=0; i < headers.length; i++){
const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase()
headers[i].setAttribute('id', headerId);
headers[i].addEventListener('click', () =>
window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`);
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase();
header.setAttribute('id', headerId);
header.addEventListener('click', () => {
window.location.hash = `#${headerId}`;
header.scrollIntoView();
});
}
// Manually scroll the user to the appropriate header if anchor navigation is used
if (scrollTargetIndex > -1) {
try{
const scrollTarget = window.location.href.substring(scrollTargetIndex + 1);
document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" });
} catch(error) {
console.error(error);
document.fonts.ready.finally(() => {
if (window.location.hash) {
const scrollTarget = document.getElementById(window.location.hash.substring(1));
scrollTarget?.scrollIntoView();
}
}
});
}).catch((error) => {
console.error(error);
tutorialWrapper.innerHTML =

View File

@@ -0,0 +1,20 @@
window.addEventListener('load', () => {
const url = window.location;
setInterval(() => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
// Create a fake DOM using the returned HTML
const domParser = new DOMParser();
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
// Update item and location trackers
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
document.getElementById('location-table').innerHTML = fakeDOM.getElementById('location-table').innerHTML;
};
ajax.open('GET', url);
ajax.send();
}, 15000)
});

View File

@@ -0,0 +1,6 @@
window.addEventListener('load', () => {
$(".table-wrapper").scrollsync({
y_sync: true,
x_sync: true
});
});

View File

@@ -118,6 +118,8 @@ const buildOptionsTable = (settings, romOpts = false) => {
const tdr = document.createElement('td');
let element = null;
const randomButton = document.createElement('button');
switch(settings[setting].type){
case 'select':
element = document.createElement('div');
@@ -138,8 +140,21 @@ const buildOptionsTable = (settings, romOpts = false) => {
}
select.appendChild(option);
});
select.addEventListener('change', (event) => updateGameSetting(event));
select.addEventListener('change', (event) => updateGameSetting(event.target));
element.appendChild(select);
// Randomize button
randomButton.innerText = '🎲';
randomButton.classList.add('randomize-button');
randomButton.setAttribute('data-key', setting);
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
randomButton.addEventListener('click', (event) => toggleRandomize(event, select));
if (currentSettings[gameName][setting] === 'random') {
randomButton.classList.add('active');
select.disabled = true;
}
element.appendChild(randomButton);
break;
case 'range':
@@ -154,15 +169,29 @@ const buildOptionsTable = (settings, romOpts = false) => {
range.value = currentSettings[gameName][setting];
range.addEventListener('change', (event) => {
document.getElementById(`${setting}-value`).innerText = event.target.value;
updateGameSetting(event);
updateGameSetting(event.target);
});
element.appendChild(range);
let rangeVal = document.createElement('span');
rangeVal.classList.add('range-value');
rangeVal.setAttribute('id', `${setting}-value`);
rangeVal.innerText = currentSettings[gameName][setting] ?? settings[setting].defaultValue;
rangeVal.innerText = currentSettings[gameName][setting] !== 'random' ?
currentSettings[gameName][setting] : settings[setting].defaultValue;
element.appendChild(rangeVal);
// Randomize button
randomButton.innerText = '🎲';
randomButton.classList.add('randomize-button');
randomButton.setAttribute('data-key', setting);
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
randomButton.addEventListener('click', (event) => toggleRandomize(event, range));
if (currentSettings[gameName][setting] === 'random') {
randomButton.classList.add('active');
range.disabled = true;
}
element.appendChild(randomButton);
break;
case 'special_range':
@@ -176,6 +205,11 @@ const buildOptionsTable = (settings, romOpts = false) => {
let presetOption = document.createElement('option');
presetOption.innerText = presetName;
presetOption.value = settings[setting].value_names[presetName];
const words = presetOption.innerText.split("_");
for (let i = 0; i < words.length; i++) {
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
}
presetOption.innerText = words.join(" ");
specialRangeSelect.appendChild(presetOption);
});
let customOption = document.createElement('option');
@@ -201,7 +235,8 @@ const buildOptionsTable = (settings, romOpts = false) => {
let specialRangeVal = document.createElement('span');
specialRangeVal.classList.add('range-value');
specialRangeVal.setAttribute('id', `${setting}-value`);
specialRangeVal.innerText = currentSettings[gameName][setting] ?? settings[setting].defaultValue;
specialRangeVal.innerText = currentSettings[gameName][setting] !== 'random' ?
currentSettings[gameName][setting] : settings[setting].defaultValue;
// Configure select event listener
specialRangeSelect.addEventListener('change', (event) => {
@@ -210,7 +245,7 @@ const buildOptionsTable = (settings, romOpts = false) => {
// Update range slider
specialRange.value = event.target.value;
document.getElementById(`${setting}-value`).innerText = event.target.value;
updateGameSetting(event);
updateGameSetting(event.target);
});
// Configure range event handler
@@ -220,13 +255,29 @@ const buildOptionsTable = (settings, romOpts = false) => {
(Object.values(settings[setting].value_names).includes(parseInt(event.target.value))) ?
parseInt(event.target.value) : 'custom';
document.getElementById(`${setting}-value`).innerText = event.target.value;
updateGameSetting(event);
updateGameSetting(event.target);
});
element.appendChild(specialRangeSelect);
specialRangeWrapper.appendChild(specialRange);
specialRangeWrapper.appendChild(specialRangeVal);
element.appendChild(specialRangeWrapper);
// Randomize button
randomButton.innerText = '🎲';
randomButton.classList.add('randomize-button');
randomButton.setAttribute('data-key', setting);
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
randomButton.addEventListener('click', (event) => toggleRandomize(
event, specialRange, specialRangeSelect)
);
if (currentSettings[gameName][setting] === 'random') {
randomButton.classList.add('active');
specialRange.disabled = true;
specialRangeSelect.disabled = true;
}
specialRangeWrapper.appendChild(randomButton);
break;
default:
@@ -243,6 +294,27 @@ const buildOptionsTable = (settings, romOpts = false) => {
return table;
};
const toggleRandomize = (event, inputElement, optionalSelectElement = null) => {
const active = event.target.classList.contains('active');
const randomButton = event.target;
if (active) {
randomButton.classList.remove('active');
inputElement.disabled = undefined;
if (optionalSelectElement) {
optionalSelectElement.disabled = undefined;
}
} else {
randomButton.classList.add('active');
inputElement.disabled = true;
if (optionalSelectElement) {
optionalSelectElement.disabled = true;
}
}
updateGameSetting(randomButton);
};
const updateBaseSetting = (event) => {
const options = JSON.parse(localStorage.getItem(gameName));
options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
@@ -250,10 +322,17 @@ const updateBaseSetting = (event) => {
localStorage.setItem(gameName, JSON.stringify(options));
};
const updateGameSetting = (event) => {
const updateGameSetting = (settingElement) => {
const options = JSON.parse(localStorage.getItem(gameName));
options[gameName][event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value, 10);
if (settingElement.classList.contains('randomize-button')) {
// If the event passed in is the randomize button, then we know what we must do.
options[gameName][settingElement.getAttribute('data-key')] = 'random';
} else {
options[gameName][settingElement.getAttribute('data-key')] = isNaN(settingElement.value) ?
settingElement.value : parseInt(settingElement.value, 10);
}
localStorage.setItem(gameName, JSON.stringify(options));
};
@@ -287,6 +366,7 @@ const generateGame = (raceMode = false) => {
weights: { player: settings },
presetData: { player: settings },
playerCount: 1,
spoiler: 3,
race: raceMode ? '1' : '0',
}).then((response) => {
window.location.href = response.data.url;

View File

@@ -0,0 +1,49 @@
window.addEventListener('load', () => {
// Reload tracker every 15 seconds
const url = window.location;
setInterval(() => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
// Create a fake DOM using the returned HTML
const domParser = new DOMParser();
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
// Update item tracker
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
// Update only counters in the location-table
let counters = document.getElementsByClassName('counter');
const fakeCounters = fakeDOM.getElementsByClassName('counter');
for (let i = 0; i < counters.length; i++) {
counters[i].innerHTML = fakeCounters[i].innerHTML;
}
};
ajax.open('GET', url);
ajax.send();
}, 15000)
// Collapsible advancement sections
const categories = document.getElementsByClassName("location-category");
for (let i = 0; i < categories.length; i++) {
let hide_id = categories[i].id.split('-')[0];
if (hide_id == 'Total') {
continue;
}
categories[i].addEventListener('click', function() {
// Toggle the advancement list
document.getElementById(hide_id).classList.toggle("hide");
// Change text of the header
const tab_header = document.getElementById(hide_id+'-header').children[0];
const orig_text = tab_header.innerHTML;
let new_text;
if (orig_text.includes("▼")) {
new_text = orig_text.replace("▼", "▲");
}
else {
new_text = orig_text.replace("▲", "▼");
}
tab_header.innerHTML = new_text;
});
}
});

View File

@@ -1,5 +1,7 @@
const adjustTableHeight = () => {
const tablesContainer = document.getElementById('tables-container');
if (!tablesContainer)
return;
const upperDistance = tablesContainer.getBoundingClientRect().top;
const containerHeight = window.innerHeight - upperDistance;
@@ -17,6 +19,14 @@ window.addEventListener('load', () => {
paging: false,
info: false,
dom: "t",
stateSave: true,
stateSaveCallback: function(settings, data) {
delete data.search;
localStorage.setItem(`DataTables_${settings.sInstance}_/tracker`, JSON.stringify(data));
},
stateLoadCallback: function(settings) {
return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`));
},
columnDefs: [
{
targets: 'hours',
@@ -63,16 +73,44 @@ window.addEventListener('load', () => {
// the tbody and render two separate tables.
});
document.getElementById('search').addEventListener('keyup', (event) => {
tables.search(event.target.value);
console.info(tables.search());
const searchBox = document.getElementById("search");
searchBox.value = tables.search();
searchBox.focus();
searchBox.select();
const doSearch = () => {
tables.search(searchBox.value);
tables.draw();
};
searchBox.addEventListener("keyup", doSearch);
window.addEventListener("keydown", (event) => {
if (!event.ctrlKey && !event.altKey && event.key.length === 1 && document.activeElement !== searchBox) {
searchBox.focus();
searchBox.select();
}
if (!event.ctrlKey && !event.altKey && !event.shiftKey && event.key === "Escape") {
if (searchBox.value !== "") {
searchBox.value = "";
doSearch();
}
searchBox.blur();
if (!document.getElementById("tables-container"))
window.scroll(0, 0);
event.preventDefault();
}
});
const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
const target_second = document.getElementById('tracker-wrapper').getAttribute('data-second') + 3;
function getSleepTimeSeconds(){
// -40 % 60 is -40, which is absolutely wrong and should burn
var sleepSeconds = (((target_second - new Date().getSeconds()) % 60) + 60) % 60;
return sleepSeconds || 60;
}
const update = () => {
const target = $("<div></div>");
const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
target.load("/tracker/" + tracker, function (response, status) {
console.log("Updating Tracker...");
target.load(location.href, function (response, status) {
if (status === "success") {
target.find(".table").each(function (i, new_table) {
const new_trs = $(new_table).find("tbody>tr");
@@ -90,19 +128,14 @@ window.addEventListener('load', () => {
console.log(response);
}
})
setTimeout(update, getSleepTimeSeconds()*1000);
}
setInterval(update, 30000);
setTimeout(update, getSleepTimeSeconds()*1000);
window.addEventListener('resize', () => {
adjustTableHeight();
tables.draw();
});
$(".table-wrapper").scrollsync({
y_sync: true,
x_sync: true
});
adjustTableHeight();
});

View File

@@ -1,82 +0,0 @@
window.addEventListener('load', () => {
// Reload tracker
const update = () => {
const room = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
const request = new Request('/api/tracker/' + room);
fetch(request)
.then(response => response.json())
.then(data => {
// update locations blocks
for (const location of data.checked_locations) {
document.getElementById(location).classList.add('acquired');
}
// update totals checks done
let total_checks_ele = document.getElementById('total-checks');
const total_checks = document.getElementsByClassName('location').length;
let checks_done = data.checked_locations.length;
total_checks_ele.innerText = 'Total Checks Done: ' + checks_done + '/' + total_checks;
// update item and icons blocks
// update icons block
if (data.icons.length > 0) {
for (let item in data.icons) {
if (data.progressive_names.length > 0) {
for (let item_category in data.progressive_names) {
let i = 0;
for (let current_item in current_name) {
if (current_item === item) {
let doc_item = document.getElementById(item_category)
doc_item.children[0].src = data.icons[item];
if (item in data.items_received) {
doc_item.children[0].classList.add('acquired');
doc_item.children[1].innerText = item_category;
}
}
}
}
} else {
if (item in data.items_received) {
let current_item = document.getElementById(item);
current_item.children[0].classList.add('acquired');
current_item.children[0].src = data.icons[item];
current_item.children[1].innerText = item;
}
}
}
} else {
for (const item in data.items_received) {
if (document.getElementById(item)) {
let current_item = document.getElementById(item);
current_item.innerText = item + data.items_received[item];
}
}
}
});
}
update()
setInterval(update, 30000);
// Collapsible regions section
const regions = document.getElementsByClassName('regions-column');
for (let i = 0; i < regions.length; i++) {
let region_name = regions[i].id;
const tab_header = document.getElementById(region_name+'-header');
const locations = document.getElementById(region_name+'-locations');
// toggle locations display
regions[i].addEventListener('click', function(event) {
if (tab_header.innerHTML.includes("▼")) {
locations.classList.remove('hidden');
// change header text
tab_header.innerHTML = tab_header.innerHTML.replace('▼', '▲');
} else {
locations.classList.add('hidden');
// change header text
tab_header.innerHTML = tab_header.innerHTML.replace('▲', '▼');
}
});
}
});

View File

@@ -1,82 +0,0 @@
window.addEventListener('load', () => {
// Reload tracker
const update = () => {
const room = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
const request = new Request('/api/tracker/' + room);
fetch(request)
.then(response => response.json())
.then(data => {
// update locations blocks
for (const location of data.checked_locations) {
document.getElementById(location).classList.add('acquired');
}
// update totals checks done
let total_checks_ele = document.getElementById('total-checks');
const total_checks = document.getElementsByClassName('location').length;
let checks_done = data.checked_locations.length;
total_checks_ele.innerText = 'Total Checks Done: ' + checks_done + '/' + total_checks;
// update item and icons blocks
// update icons block
if (data.icons.length > 0) {
for (let item in data.icons) {
if (data.progressive_names.length > 0) {
for (let item_category in data.progressive_names) {
let i = 0;
for (let current_item in current_name) {
if (current_item === item) {
let doc_item = document.getElementById(item_category)
doc_item.children[0].src = data.icons[item];
if (item in data.items_received) {
doc_item.children[0].classList.add('acquired');
doc_item.children[1].innerText = item_category;
}
}
}
}
} else {
if (item in data.items_received) {
let current_item = document.getElementById(item);
current_item.children[0].classList.add('acquired');
current_item.children[0].src = data.icons[item];
current_item.children[1].innerText = item;
}
}
}
} else {
for (const item in data.items_received) {
if (document.getElementById(item)) {
let current_item = document.getElementById(item);
current_item.innerText = item + data.items_received[item];
}
}
}
});
}
update()
setInterval(update, 30000);
// Collapsible regions section
const regions = document.getElementsByClassName('regions-column');
for (let i = 0; i < regions.length; i++) {
let region_name = regions[i].id;
const tab_header = document.getElementById(region_name+'-header');
const locations = document.getElementById(region_name+'-locations');
// toggle locations display
regions[i].addEventListener('click', function(event) {
if (tab_header.innerHTML.includes("▼")) {
locations.classList.remove('hidden');
// change header text
tab_header.innerHTML = tab_header.innerHTML.replace('▼', '▲');
} else {
locations.classList.add('hidden');
// change header text
tab_header.innerHTML = tab_header.innerHTML.replace('▲', '▼');
}
});
}
});

View File

@@ -27,25 +27,28 @@ window.addEventListener('load', () => {
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
adjustHeaderWidth();
const title = document.querySelector('h1')
if (title) {
document.title = title.textContent;
}
// Reset the id of all header divs to something nicer
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/);
for (let i=0; i < headers.length; i++){
const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase()
headers[i].setAttribute('id', headerId);
headers[i].addEventListener('click', () =>
window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`);
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase();
header.setAttribute('id', headerId);
header.addEventListener('click', () => {
window.location.hash = `#${headerId}`;
header.scrollIntoView();
});
}
// Manually scroll the user to the appropriate header if anchor navigation is used
if (scrollTargetIndex > -1) {
try{
const scrollTarget = window.location.href.substring(scrollTargetIndex + 1);
document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" });
} catch(error) {
console.error(error);
document.fonts.ready.finally(() => {
if (window.location.hash) {
const scrollTarget = document.getElementById(window.location.hash.substring(1));
scrollTarget?.scrollIntoView();
}
}
});
}).catch((error) => {
console.error(error);
tutorialWrapper.innerHTML =

View File

@@ -6,6 +6,7 @@ window.addEventListener('load', () => {
"order": [[ 3, "desc" ]],
"info": false,
"dom": "t",
"stateSave": true,
});
$("#seeds-table").DataTable({
"paging": false,
@@ -13,5 +14,6 @@ window.addEventListener('load', () => {
"order": [[ 2, "desc" ]],
"info": false,
"dom": "t",
"stateSave": true,
});
});

View File

@@ -78,19 +78,20 @@ const createDefaultSettings = (settingData) => {
break;
case 'range':
case 'special_range':
for (let i = setting.min; i <= setting.max; ++i){
newSettings[game][gameSetting][i] =
(setting.hasOwnProperty('defaultValue') && setting.defaultValue === i) ? 25 : 0;
}
newSettings[game][gameSetting]['random'] = 0;
newSettings[game][gameSetting]['random-low'] = 0;
newSettings[game][gameSetting]['random-high'] = 0;
if (setting.hasOwnProperty('defaultValue')) {
newSettings[game][gameSetting][setting.defaultValue] = 25;
} else {
newSettings[game][gameSetting][setting.min] = 25;
}
break;
case 'items-list':
case 'locations-list':
case 'custom-list':
newSettings[game][gameSetting] = [];
newSettings[game][gameSetting] = setting.defaultValue;
break;
default:
@@ -100,6 +101,7 @@ const createDefaultSettings = (settingData) => {
newSettings[game].start_inventory = {};
newSettings[game].exclude_locations = [];
newSettings[game].priority_locations = [];
newSettings[game].local_items = [];
newSettings[game].non_local_items = [];
newSettings[game].start_hints = [];
@@ -135,21 +137,28 @@ const buildUI = (settingData) => {
expandButton.classList.add('invisible');
gameDiv.appendChild(expandButton);
const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings);
settingData.games[game].gameItems.sort((a, b) => (a > b ? 1 : (a < b ? -1 : 0)));
settingData.games[game].gameLocations.sort((a, b) => (a > b ? 1 : (a < b ? -1 : 0)));
const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings,
settingData.games[game].gameItems, settingData.games[game].gameLocations);
gameDiv.appendChild(weightedSettingsDiv);
const itemsDiv = buildItemsDiv(game, settingData.games[game].gameItems);
gameDiv.appendChild(itemsDiv);
const itemPoolDiv = buildItemsDiv(game, settingData.games[game].gameItems);
gameDiv.appendChild(itemPoolDiv);
const hintsDiv = buildHintsDiv(game, settingData.games[game].gameItems, settingData.games[game].gameLocations);
gameDiv.appendChild(hintsDiv);
const locationsDiv = buildLocationsDiv(game, settingData.games[game].gameLocations);
gameDiv.appendChild(locationsDiv);
gamesWrapper.appendChild(gameDiv);
collapseButton.addEventListener('click', () => {
collapseButton.classList.add('invisible');
weightedSettingsDiv.classList.add('invisible');
itemsDiv.classList.add('invisible');
itemPoolDiv.classList.add('invisible');
hintsDiv.classList.add('invisible');
expandButton.classList.remove('invisible');
});
@@ -157,7 +166,7 @@ const buildUI = (settingData) => {
expandButton.addEventListener('click', () => {
collapseButton.classList.remove('invisible');
weightedSettingsDiv.classList.remove('invisible');
itemsDiv.classList.remove('invisible');
itemPoolDiv.classList.remove('invisible');
hintsDiv.classList.remove('invisible');
expandButton.classList.add('invisible');
});
@@ -225,7 +234,7 @@ const buildGameChoice = (games) => {
gameChoiceDiv.appendChild(table);
};
const buildWeightedSettingsDiv = (game, settings) => {
const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => {
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
const settingsWrapper = document.createElement('div');
settingsWrapper.classList.add('settings-wrapper');
@@ -267,7 +276,7 @@ const buildWeightedSettingsDiv = (game, settings) => {
range.setAttribute('data-type', setting.type);
range.setAttribute('min', 0);
range.setAttribute('max', 50);
range.addEventListener('change', updateGameSetting);
range.addEventListener('change', updateRangeSetting);
range.value = currentSettings[game][settingName][option.value];
tdMiddle.appendChild(range);
tr.appendChild(tdMiddle);
@@ -293,33 +302,33 @@ const buildWeightedSettingsDiv = (game, settings) => {
if (((setting.max - setting.min) + 1) < 11) {
for (let i=setting.min; i <= setting.max; ++i) {
const tr = document.createElement('tr');
const tdLeft = document.createElement('td');
tdLeft.classList.add('td-left');
tdLeft.innerText = i;
tr.appendChild(tdLeft);
const tdLeft = document.createElement('td');
tdLeft.classList.add('td-left');
tdLeft.innerText = i;
tr.appendChild(tdLeft);
const tdMiddle = document.createElement('td');
tdMiddle.classList.add('td-middle');
const range = document.createElement('input');
range.setAttribute('type', 'range');
range.setAttribute('id', `${game}-${settingName}-${i}-range`);
range.setAttribute('data-game', game);
range.setAttribute('data-setting', settingName);
range.setAttribute('data-option', i);
range.setAttribute('min', 0);
range.setAttribute('max', 50);
range.addEventListener('change', updateGameSetting);
range.value = currentSettings[game][settingName][i];
tdMiddle.appendChild(range);
tr.appendChild(tdMiddle);
const tdMiddle = document.createElement('td');
tdMiddle.classList.add('td-middle');
const range = document.createElement('input');
range.setAttribute('type', 'range');
range.setAttribute('id', `${game}-${settingName}-${i}-range`);
range.setAttribute('data-game', game);
range.setAttribute('data-setting', settingName);
range.setAttribute('data-option', i);
range.setAttribute('min', 0);
range.setAttribute('max', 50);
range.addEventListener('change', updateRangeSetting);
range.value = currentSettings[game][settingName][i] || 0;
tdMiddle.appendChild(range);
tr.appendChild(tdMiddle);
const tdRight = document.createElement('td');
tdRight.setAttribute('id', `${game}-${settingName}-${i}`)
tdRight.classList.add('td-right');
tdRight.innerText = range.value;
tr.appendChild(tdRight);
const tdRight = document.createElement('td');
tdRight.setAttribute('id', `${game}-${settingName}-${i}`)
tdRight.classList.add('td-right');
tdRight.innerText = range.value;
tr.appendChild(tdRight);
rangeTbody.appendChild(tr);
rangeTbody.appendChild(tr);
}
} else {
const hintText = document.createElement('p');
@@ -376,7 +385,7 @@ const buildWeightedSettingsDiv = (game, settings) => {
range.setAttribute('data-option', option);
range.setAttribute('min', 0);
range.setAttribute('max', 50);
range.addEventListener('change', updateGameSetting);
range.addEventListener('change', updateRangeSetting);
range.value = currentSettings[game][settingName][parseInt(option, 10)];
tdMiddle.appendChild(range);
tr.appendChild(tdMiddle);
@@ -401,11 +410,17 @@ const buildWeightedSettingsDiv = (game, settings) => {
tr.appendChild(tdDelete);
rangeTbody.appendChild(tr);
// Save new option to settings
range.dispatchEvent(new Event('change'));
});
Object.keys(currentSettings[game][settingName]).forEach((option) => {
if (currentSettings[game][settingName][option] > 0) {
const tr = document.createElement('tr');
// These options are statically generated below, and should always appear even if they are deleted
// from localStorage
if (['random-low', 'random', 'random-high'].includes(option)) { return; }
const tr = document.createElement('tr');
const tdLeft = document.createElement('td');
tdLeft.classList.add('td-left');
tdLeft.innerText = option;
@@ -421,7 +436,7 @@ const buildWeightedSettingsDiv = (game, settings) => {
range.setAttribute('data-option', option);
range.setAttribute('min', 0);
range.setAttribute('max', 50);
range.addEventListener('change', updateGameSetting);
range.addEventListener('change', updateRangeSetting);
range.value = currentSettings[game][settingName][parseInt(option, 10)];
tdMiddle.appendChild(range);
tr.appendChild(tdMiddle);
@@ -439,14 +454,15 @@ const buildWeightedSettingsDiv = (game, settings) => {
deleteButton.innerText = '❌';
deleteButton.addEventListener('click', () => {
range.value = 0;
range.dispatchEvent(new Event('change'));
const changeEvent = new Event('change');
changeEvent.action = 'rangeDelete';
range.dispatchEvent(changeEvent);
rangeTbody.removeChild(tr);
});
tdDelete.appendChild(deleteButton);
tr.appendChild(tdDelete);
rangeTbody.appendChild(tr);
}
});
}
@@ -454,7 +470,17 @@ const buildWeightedSettingsDiv = (game, settings) => {
const tr = document.createElement('tr');
const tdLeft = document.createElement('td');
tdLeft.classList.add('td-left');
tdLeft.innerText = option;
switch(option){
case 'random':
tdLeft.innerText = 'Random';
break;
case 'random-low':
tdLeft.innerText = "Random (Low)";
break;
case 'random-high':
tdLeft.innerText = "Random (High)";
break;
}
tr.appendChild(tdLeft);
const tdMiddle = document.createElement('td');
@@ -467,7 +493,7 @@ const buildWeightedSettingsDiv = (game, settings) => {
range.setAttribute('data-option', option);
range.setAttribute('min', 0);
range.setAttribute('max', 50);
range.addEventListener('change', updateGameSetting);
range.addEventListener('change', updateRangeSetting);
range.value = currentSettings[game][settingName][option];
tdMiddle.appendChild(range);
tr.appendChild(tdMiddle);
@@ -485,15 +511,108 @@ const buildWeightedSettingsDiv = (game, settings) => {
break;
case 'items-list':
// TODO
const itemsList = document.createElement('div');
itemsList.classList.add('simple-list');
Object.values(gameItems).forEach((item) => {
const itemRow = document.createElement('div');
itemRow.classList.add('list-row');
const itemLabel = document.createElement('label');
itemLabel.setAttribute('for', `${game}-${settingName}-${item}`)
const itemCheckbox = document.createElement('input');
itemCheckbox.setAttribute('id', `${game}-${settingName}-${item}`);
itemCheckbox.setAttribute('type', 'checkbox');
itemCheckbox.setAttribute('data-game', game);
itemCheckbox.setAttribute('data-setting', settingName);
itemCheckbox.setAttribute('data-option', item.toString());
itemCheckbox.addEventListener('change', updateListSetting);
if (currentSettings[game][settingName].includes(item)) {
itemCheckbox.setAttribute('checked', '1');
}
const itemName = document.createElement('span');
itemName.innerText = item.toString();
itemLabel.appendChild(itemCheckbox);
itemLabel.appendChild(itemName);
itemRow.appendChild(itemLabel);
itemsList.appendChild((itemRow));
});
settingWrapper.appendChild(itemsList);
break;
case 'locations-list':
// TODO
const locationsList = document.createElement('div');
locationsList.classList.add('simple-list');
Object.values(gameLocations).forEach((location) => {
const locationRow = document.createElement('div');
locationRow.classList.add('list-row');
const locationLabel = document.createElement('label');
locationLabel.setAttribute('for', `${game}-${settingName}-${location}`)
const locationCheckbox = document.createElement('input');
locationCheckbox.setAttribute('id', `${game}-${settingName}-${location}`);
locationCheckbox.setAttribute('type', 'checkbox');
locationCheckbox.setAttribute('data-game', game);
locationCheckbox.setAttribute('data-setting', settingName);
locationCheckbox.setAttribute('data-option', location.toString());
locationCheckbox.addEventListener('change', updateListSetting);
if (currentSettings[game][settingName].includes(location)) {
locationCheckbox.setAttribute('checked', '1');
}
const locationName = document.createElement('span');
locationName.innerText = location.toString();
locationLabel.appendChild(locationCheckbox);
locationLabel.appendChild(locationName);
locationRow.appendChild(locationLabel);
locationsList.appendChild((locationRow));
});
settingWrapper.appendChild(locationsList);
break;
case 'custom-list':
// TODO
const customList = document.createElement('div');
customList.classList.add('simple-list');
Object.values(settings[settingName].options).forEach((listItem) => {
const customListRow = document.createElement('div');
customListRow.classList.add('list-row');
const customItemLabel = document.createElement('label');
customItemLabel.setAttribute('for', `${game}-${settingName}-${listItem}`)
const customItemCheckbox = document.createElement('input');
customItemCheckbox.setAttribute('id', `${game}-${settingName}-${listItem}`);
customItemCheckbox.setAttribute('type', 'checkbox');
customItemCheckbox.setAttribute('data-game', game);
customItemCheckbox.setAttribute('data-setting', settingName);
customItemCheckbox.setAttribute('data-option', listItem.toString());
customItemCheckbox.addEventListener('change', updateListSetting);
if (currentSettings[game][settingName].includes(listItem)) {
customItemCheckbox.setAttribute('checked', '1');
}
const customItemName = document.createElement('span');
customItemName.innerText = listItem.toString();
customItemLabel.appendChild(customItemCheckbox);
customItemLabel.appendChild(customItemName);
customListRow.appendChild(customItemLabel);
customList.appendChild((customListRow));
});
settingWrapper.appendChild(customList);
break;
default:
@@ -719,21 +838,22 @@ const buildHintsDiv = (game, items, locations) => {
const hintsDescription = document.createElement('p');
hintsDescription.classList.add('setting-description');
hintsDescription.innerText = 'Choose any items or locations to begin the game with the knowledge of where those ' +
' items are, or what those locations contain. Excluded locations will not contain progression items.';
' items are, or what those locations contain.';
hintsDiv.appendChild(hintsDescription);
const itemHintsContainer = document.createElement('div');
itemHintsContainer.classList.add('hints-container');
// Item Hints
const itemHintsWrapper = document.createElement('div');
itemHintsWrapper.classList.add('hints-wrapper');
itemHintsWrapper.innerText = 'Starting Item Hints';
const itemHintsDiv = document.createElement('div');
itemHintsDiv.classList.add('item-container');
itemHintsDiv.classList.add('simple-list');
items.forEach((item) => {
const itemDiv = document.createElement('div');
itemDiv.classList.add('hint-div');
const itemRow = document.createElement('div');
itemRow.classList.add('list-row');
const itemLabel = document.createElement('label');
itemLabel.setAttribute('for', `${game}-start_hints-${item}`);
@@ -747,29 +867,30 @@ const buildHintsDiv = (game, items, locations) => {
if (currentSettings[game].start_hints.includes(item)) {
itemCheckbox.setAttribute('checked', 'true');
}
itemCheckbox.addEventListener('change', hintChangeHandler);
itemCheckbox.addEventListener('change', updateListSetting);
itemLabel.appendChild(itemCheckbox);
const itemName = document.createElement('span');
itemName.innerText = item;
itemLabel.appendChild(itemName);
itemDiv.appendChild(itemLabel);
itemHintsDiv.appendChild(itemDiv);
itemRow.appendChild(itemLabel);
itemHintsDiv.appendChild(itemRow);
});
itemHintsWrapper.appendChild(itemHintsDiv);
itemHintsContainer.appendChild(itemHintsWrapper);
// Starting Location Hints
const locationHintsWrapper = document.createElement('div');
locationHintsWrapper.classList.add('hints-wrapper');
locationHintsWrapper.innerText = 'Starting Location Hints';
const locationHintsDiv = document.createElement('div');
locationHintsDiv.classList.add('item-container');
locationHintsDiv.classList.add('simple-list');
locations.forEach((location) => {
const locationDiv = document.createElement('div');
locationDiv.classList.add('hint-div');
const locationRow = document.createElement('div');
locationRow.classList.add('list-row');
const locationLabel = document.createElement('label');
locationLabel.setAttribute('for', `${game}-start_location_hints-${location}`);
@@ -783,29 +904,89 @@ const buildHintsDiv = (game, items, locations) => {
if (currentSettings[game].start_location_hints.includes(location)) {
locationCheckbox.setAttribute('checked', '1');
}
locationCheckbox.addEventListener('change', hintChangeHandler);
locationCheckbox.addEventListener('change', updateListSetting);
locationLabel.appendChild(locationCheckbox);
const locationName = document.createElement('span');
locationName.innerText = location;
locationLabel.appendChild(locationName);
locationDiv.appendChild(locationLabel);
locationHintsDiv.appendChild(locationDiv);
locationRow.appendChild(locationLabel);
locationHintsDiv.appendChild(locationRow);
});
locationHintsWrapper.appendChild(locationHintsDiv);
itemHintsContainer.appendChild(locationHintsWrapper);
hintsDiv.appendChild(itemHintsContainer);
return hintsDiv;
};
const buildLocationsDiv = (game, locations) => {
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
locations.sort(); // Sort alphabetical, in-place
const locationsDiv = document.createElement('div');
locationsDiv.classList.add('locations-div');
const locationsHeader = document.createElement('h3');
locationsHeader.innerText = 'Priority & Exclusion Locations';
locationsDiv.appendChild(locationsHeader);
const locationsDescription = document.createElement('p');
locationsDescription.classList.add('setting-description');
locationsDescription.innerText = 'Priority locations guarantee a progression item will be placed there while ' +
'excluded locations will not contain progression or useful items.';
locationsDiv.appendChild(locationsDescription);
const locationsContainer = document.createElement('div');
locationsContainer.classList.add('locations-container');
// Priority Locations
const priorityLocationsWrapper = document.createElement('div');
priorityLocationsWrapper.classList.add('locations-wrapper');
priorityLocationsWrapper.innerText = 'Priority Locations';
const priorityLocationsDiv = document.createElement('div');
priorityLocationsDiv.classList.add('simple-list');
locations.forEach((location) => {
const locationRow = document.createElement('div');
locationRow.classList.add('list-row');
const locationLabel = document.createElement('label');
locationLabel.setAttribute('for', `${game}-priority_locations-${location}`);
const locationCheckbox = document.createElement('input');
locationCheckbox.setAttribute('type', 'checkbox');
locationCheckbox.setAttribute('id', `${game}-priority_locations-${location}`);
locationCheckbox.setAttribute('data-game', game);
locationCheckbox.setAttribute('data-setting', 'priority_locations');
locationCheckbox.setAttribute('data-option', location);
if (currentSettings[game].priority_locations.includes(location)) {
locationCheckbox.setAttribute('checked', '1');
}
locationCheckbox.addEventListener('change', updateListSetting);
locationLabel.appendChild(locationCheckbox);
const locationName = document.createElement('span');
locationName.innerText = location;
locationLabel.appendChild(locationName);
locationRow.appendChild(locationLabel);
priorityLocationsDiv.appendChild(locationRow);
});
priorityLocationsWrapper.appendChild(priorityLocationsDiv);
locationsContainer.appendChild(priorityLocationsWrapper);
// Exclude Locations
const excludeLocationsWrapper = document.createElement('div');
excludeLocationsWrapper.classList.add('hints-wrapper');
excludeLocationsWrapper.classList.add('locations-wrapper');
excludeLocationsWrapper.innerText = 'Exclude Locations';
const excludeLocationsDiv = document.createElement('div');
excludeLocationsDiv.classList.add('item-container');
excludeLocationsDiv.classList.add('simple-list');
locations.forEach((location) => {
const locationDiv = document.createElement('div');
locationDiv.classList.add('hint-div');
const locationRow = document.createElement('div');
locationRow.classList.add('list-row');
const locationLabel = document.createElement('label');
locationLabel.setAttribute('for', `${game}-exclude_locations-${location}`);
@@ -819,40 +1000,22 @@ const buildHintsDiv = (game, items, locations) => {
if (currentSettings[game].exclude_locations.includes(location)) {
locationCheckbox.setAttribute('checked', '1');
}
locationCheckbox.addEventListener('change', hintChangeHandler);
locationCheckbox.addEventListener('change', updateListSetting);
locationLabel.appendChild(locationCheckbox);
const locationName = document.createElement('span');
locationName.innerText = location;
locationLabel.appendChild(locationName);
locationDiv.appendChild(locationLabel);
excludeLocationsDiv.appendChild(locationDiv);
locationRow.appendChild(locationLabel);
excludeLocationsDiv.appendChild(locationRow);
});
excludeLocationsWrapper.appendChild(excludeLocationsDiv);
itemHintsContainer.appendChild(excludeLocationsWrapper);
locationsContainer.appendChild(excludeLocationsWrapper);
hintsDiv.appendChild(itemHintsContainer);
return hintsDiv;
};
const hintChangeHandler = (evt) => {
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
const game = evt.target.getAttribute('data-game');
const setting = evt.target.getAttribute('data-setting');
const option = evt.target.getAttribute('data-option');
if (evt.target.checked) {
if (!currentSettings[game][setting].includes(option)) {
currentSettings[game][setting].push(option);
}
} else {
if (currentSettings[game][setting].includes(option)) {
currentSettings[game][setting].splice(currentSettings[game][setting].indexOf(option), 1);
}
}
localStorage.setItem('weighted-settings', JSON.stringify(currentSettings));
locationsDiv.appendChild(locationsContainer);
return locationsDiv;
};
const updateVisibleGames = () => {
@@ -898,14 +1061,37 @@ const updateBaseSetting = (event) => {
localStorage.setItem('weighted-settings', JSON.stringify(settings));
};
const updateGameSetting = (evt) => {
const updateRangeSetting = (evt) => {
const options = JSON.parse(localStorage.getItem('weighted-settings'));
const game = evt.target.getAttribute('data-game');
const setting = evt.target.getAttribute('data-setting');
const option = evt.target.getAttribute('data-option');
document.getElementById(`${game}-${setting}-${option}`).innerText = evt.target.value;
options[game][setting][option] = isNaN(evt.target.value) ?
evt.target.value : parseInt(evt.target.value, 10);
if (evt.action && evt.action === 'rangeDelete') {
delete options[game][setting][option];
} else {
options[game][setting][option] = parseInt(evt.target.value, 10);
}
localStorage.setItem('weighted-settings', JSON.stringify(options));
};
const updateListSetting = (evt) => {
const options = JSON.parse(localStorage.getItem('weighted-settings'));
const game = evt.target.getAttribute('data-game');
const setting = evt.target.getAttribute('data-setting');
const option = evt.target.getAttribute('data-option');
if (evt.target.checked) {
// If the option is to be enabled and it is already enabled, do nothing
if (options[game][setting].includes(option)) { return; }
options[game][setting].push(option);
} else {
// If the option is to be disabled and it is already disabled, do nothing
if (!options[game][setting].includes(option)) { return; }
options[game][setting].splice(options[game][setting].indexOf(option), 1);
}
localStorage.setItem('weighted-settings', JSON.stringify(options));
};
@@ -1013,6 +1199,7 @@ const generateGame = (raceMode = false) => {
weights: { player: JSON.stringify(settings) },
presetData: { player: JSON.stringify(settings) },
playerCount: 1,
spoiler: 3,
race: raceMode ? '1' : '0',
}).then((response) => {
window.location.href = response.data.url;

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -0,0 +1,30 @@
#player-tracker-wrapper{
margin: 0;
}
#inventory-table{
padding: 8px 10px 2px 6px;
background-color: #42b149;
border-radius: 4px;
border: 2px solid black;
}
#inventory-table tr.column-headers td {
font-size: 1rem;
padding: 0 5rem 0 0;
}
#inventory-table td{
padding: 0 0.5rem 0.5rem;
font-family: LexendDeca-Light, monospace;
font-size: 2.5rem;
color: #ffffff;
}
#inventory-table td img{
vertical-align: middle;
}
.hide {
display: none;
}

View File

@@ -105,6 +105,9 @@ h5, h6{
margin-bottom: 20px;
background-color: #ffff00;
}
.user-message a{
color: #ff7700;
}
.interactive{
color: #ffef00;

View File

@@ -55,4 +55,6 @@
border: 1px solid #2a6c2f;
border-radius: 6px;
color: #000000;
overflow-y: auto;
max-height: 400px;
}

View File

@@ -15,3 +15,33 @@
padding-left: 0.5rem;
color: #dfedc6;
}
@media all and (max-width: 900px) {
#island-footer{
font-size: 17px;
font-size: 2vw;
}
}
@media all and (max-width: 768px) {
#island-footer{
font-size: 15px;
font-size: 2vw;
}
}
@media all and (max-width: 650px) {
#island-footer{
font-size: 13px;
font-size: 2vw;
}
}
@media all and (max-width: 580px) {
#island-footer{
font-size: 11px;
font-size: 2vw;
}
}
@media all and (max-width: 512px) {
#island-footer{
font-size: 9px;
font-size: 2vw;
}
}

View File

@@ -21,7 +21,6 @@ html{
margin-right: auto;
margin-top: 10px;
height: 140px;
z-index: 10;
}
#landing-header h4{
@@ -223,7 +222,7 @@ html{
}
#landing{
width: 700px;
max-width: 700px;
min-height: 280px;
margin-left: auto;
margin-right: auto;

View File

@@ -9,7 +9,7 @@
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding: 3px 3px 10px;
width: 448px;
width: 480px;
background-color: rgb(60, 114, 157);
}
@@ -46,7 +46,7 @@
}
#location-table{
width: 448px;
width: 480px;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-bottom: 2px solid #000000;
@@ -108,7 +108,7 @@
}
#location-table td:first-child {
width: 272px;
width: 300px;
}
.location-category td:first-child {

View File

@@ -116,6 +116,10 @@ html{
flex-grow: 1;
}
#player-settings table select:disabled{
background-color: lightgray;
}
#player-settings table .range-container{
display: flex;
flex-direction: row;
@@ -138,12 +142,27 @@ html{
#player-settings table .special-range-wrapper{
display: flex;
flex-direction: row;
margin-top: 0.25rem;
}
#player-settings table .special-range-wrapper input[type=range]{
flex-grow: 1;
}
#player-settings table .randomize-button {
max-height: 24px;
line-height: 16px;
padding: 2px 8px;
margin: 0 0 0 0.25rem;
font-size: 12px;
border: 1px solid black;
border-radius: 3px;
}
#player-settings table .randomize-button.active {
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
}
#player-settings table label{
display: block;
min-width: 200px;

View File

@@ -9,8 +9,8 @@
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding: 3px 3px 10px;
width: 384px;
background-color: #8d60a7;
width: 500px;
background-color: #525494;
}
#inventory-table td{
@@ -20,11 +20,20 @@
vertical-align: middle;
}
#inventory-table td.title{
padding-top: 10px;
height: 20px;
font-family: "JuraBook", monospace;
font-size: 16px;
font-weight: bold;
}
#inventory-table img{
height: 100%;
max-width: 40px;
max-height: 40px;
filter: grayscale(100%) contrast(75%) brightness(30%);
border: 1px solid #000000;
filter: grayscale(100%) contrast(75%) brightness(20%);
}
#inventory-table img.acquired{
@@ -36,24 +45,24 @@
}
#inventory-table div.item-count {
position: absolute;
color: white;
font-family: "Minecraftia", monospace;
text-align: left;
color: black;
font-family: "JuraBook", monospace;
font-weight: bold;
bottom: 0;
right: 0;
}
#location-table{
width: 384px;
width: 500px;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-bottom: 2px solid #000000;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
background-color: #8d60a7;
padding: 0 3px 3px;
font-size: 14px;
background-color: #525494;
padding: 10px 3px 3px;
font-family: "JuraBook", monospace;
font-size: 16px;
font-weight: bold;
cursor: default;
}
@@ -89,7 +98,7 @@
}
#location-table tbody.locations {
font-size: 12px;
font-size: 16px;
}
#location-table td.location-name {

View File

@@ -1,5 +1,7 @@
html{
padding-top: 110px;
scroll-padding-top: 100px;
scroll-behavior: smooth;
}
#base-header{
@@ -28,6 +30,8 @@ html{
}
#base-header-right{
display: flex;
flex-direction: row;
margin-top: 4px;
}
@@ -40,7 +44,7 @@ html{
margin-top: 4px;
}
#base-header a{
#base-header a, #base-header-mobile-menu a, #base-header-popover-text{
color: #2f6b83;
text-decoration: none;
cursor: pointer;
@@ -49,3 +53,126 @@ html{
font-family: LondrinaSolid-Light, sans-serif;
text-transform: uppercase;
}
#base-header-right-mobile{
display: none;
margin-top: 2rem;
margin-right: 1rem;
}
#base-header-mobile-menu{
display: none;
flex-direction: column;
background-color: #ffffff;
text-align: center;
overflow-y: auto;
z-index: 10000;
width: 100vw;
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
position: absolute;
top: 7rem;
right: 0;
}
#base-header-mobile-menu a{
padding: 3rem 1.5rem;
font-size: 4rem;
line-height: 5rem;
color: #699ca8;
border-top: 1px solid #d3d3d3;
}
#base-header-mobile-menu :first-child, #base-header-popover-menu :first-child{
border-top: none;
}
#base-header-right-mobile img{
height: 3rem;
}
#base-header-popover-menu{
display: none;
flex-direction: column;
position: absolute;
background-color: #fff;
margin-left: -108px;
margin-top: 2.25rem;
border-radius: 10px;
border-left: 2px solid #d0ebe6;
border-bottom: 2px solid #d0ebe6;
border-right: 1px solid #d0ebe6;
filter: drop-shadow(-6px 6px 2px #2e3e83);
}
#base-header-popover-menu a{
color: #699ca8;
border-top: 1px solid #d3d3d3;
text-align: center;
font-size: 1.5rem;
line-height: 3rem;
margin-right: 2px;
padding: 0.25rem 1rem;
}
#base-header-popover-icon {
width: 14px;
margin-bottom: 3px;
margin-left: 2px;
}
@media all and (max-width: 960px), only screen and (max-device-width: 768px) {
#base-header-right{
display: none;
}
#base-header-right-mobile{
display: unset;
}
}
@media all and (max-width: 960px){
#base-header-right-mobile{
margin-top: 0.5rem;
margin-right: 0;
}
#base-header-right-mobile img{
height: 1.5rem;
}
#base-header-mobile-menu{
top: 3.3rem;
width: unset;
border-left: 2px solid #d0ebe6;
border-bottom: 2px solid #d0ebe6;
filter: drop-shadow(-6px 6px 2px #2e3e83);
border-top-left-radius: 10px;
}
#base-header-mobile-menu a{
font-size: 1.5rem;
line-height: 3rem;
margin: 0;
padding: 0.25rem 1rem;
}
}
@media only screen and (max-device-width: 768px){
html{
padding-top: 260px;
scroll-padding-top: 230px;
}
#base-header{
height: 200px;
background-size: auto 200px;
}
#base-header #site-title img{
height: calc(38px * 2);
margin-top: 30px;
margin-left: 20px;
}
}

View File

@@ -52,6 +52,7 @@ pre{
pre code{
border: none;
display: block;
}
code{

View File

@@ -52,6 +52,7 @@ pre{
pre code{
border: none;
display: block;
}
code{

View File

@@ -52,6 +52,7 @@ pre{
pre code{
border: none;
display: block;
}
code{

View File

@@ -52,6 +52,7 @@ pre{
pre code{
border: none;
display: block;
}
code{

View File

@@ -52,6 +52,7 @@ pre{
pre code{
border: none;
display: block;
}
code{

View File

@@ -53,6 +53,7 @@ pre{
pre code{
border: none;
display: block;
}
code{

View File

@@ -52,6 +52,7 @@ pre{
pre code{
border: none;
display: block;
}
code{

View File

@@ -50,6 +50,7 @@ pre{
pre code{
border: none;
display: block;
}
code{

View File

@@ -0,0 +1,195 @@
#player-tracker-wrapper{
margin: 0;
}
#inventory-table{
border-top: 2px solid #000000;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding: 3px 3px 10px;
width: 374px;
background-color: #8d60a7;
display: grid;
grid-template-rows: repeat(5, 48px);
}
#inventory-table img{
display: block;
}
#inventory-table div.table-row{
display: grid;
grid-template-columns: repeat(5, 1fr);
}
#inventory-table div.C1{
grid-column: 1;
place-content: center;
place-items: center;
display: flex;
}
#inventory-table div.C2{
grid-column: 2;
place-content: center;
place-items: center;
display: flex;
}
#inventory-table div.C3{
grid-column: 3;
place-content: center;
place-items: center;
display: flex;
}
#inventory-table div.C4{
grid-column: 4;
place-content: center;
place-items: center;
display: flex;
}
#inventory-table div.C5{
grid-column: 5;
place-content: center;
place-items: center;
display: flex;
}
#inventory-table img{
max-width: 40px;
max-height: 40px;
filter: grayscale(100%) contrast(75%) brightness(30%);
}
#inventory-table img.acquired{
filter: none;
}
#inventory-table img.acquired.purple{ /*00FFFF*/
filter: hue-rotate(270deg) saturate(6) brightness(0.8);
}
#inventory-table img.acquired.cyan{ /*FF00FF*/
filter: hue-rotate(138deg) saturate(10) brightness(0.8);
}
#inventory-table img.acquired.green{ /*32CD32*/
filter: hue-rotate(84deg) saturate(10) brightness(0.7);
}
#inventory-table div.image-stack{
display: grid;
position: relative;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
#inventory-table div.image-stack div.stack-back{
grid-column: 1;
grid-row: 1;
}
#inventory-table div.image-stack div.stack-front{
grid-column: 1;
grid-row: 1;
display: grid;
grid-template-columns: 20px 20px;
grid-template-rows: 20px 20px;
}
#inventory-table div.image-stack div.stack-top-left{
grid-column: 1;
grid-row: 1;
z-index: 1;
}
#inventory-table div.image-stack div.stack-top-right{
grid-column: 2;
grid-row: 1;
z-index: 1;
}
#inventory-table div.image-stack div.stack-bottum-left{
grid-column: 1;
grid-row: 2;
z-index: 1;
}
#inventory-table div.image-stack div.stack-bottum-right{
grid-column: 2;
grid-row: 2;
z-index: 1;
}
#inventory-table div.image-stack div.stack-front img{
width: 20px;
height: 20px;
}
#inventory-table div.counted-item{
position: relative;
}
#inventory-table div.item-count{
position: absolute;
color: white;
font-family: "Minecraftia", monospace;
font-weight: bold;
bottom: 0;
right: 0;
}
#location-table{
width: 384px;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-bottom: 2px solid #000000;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
background-color: #8d60a7;
padding: 0 3px 3px;
font-size: 14px;
cursor: default;
}
#location-table th{
vertical-align: middle;
text-align: left;
padding-right: 10px;
}
#location-table td{
padding-top: 2px;
padding-bottom: 2px;
line-height: 20px;
}
#location-table td.counter{
text-align: right;
font-size: 14px;
}
#location-table td.toggle-arrow{
text-align: right;
}
#location-table tr#Total-header{
font-weight: bold;
}
#location-table img{
height: 100%;
max-width: 30px;
max-height: 30px;
}
#location-table tbody.locations{
font-size: 12px;
}
#location-table td.location-name{
padding-left: 16px;
}
.hide{
display: none;
}

View File

@@ -51,17 +51,6 @@ table.dataTable{
color: #000000;
}
table.dataTable img.icon{
height: 100%;
max-width: 60px;
max-height: 60px;
filter: grayscale(100%) contrast(75%) brightness(50%);
}
table.dataTable img.acquired{
filter: none;
}
table.dataTable thead{
font-family: LexendDeca-Regular, sans-serif;
}
@@ -130,6 +119,33 @@ img.alttp-sprite {
background-color: #d3c97d;
}
#tracker-navigation {
display: inline-flex;
background-color: #b0a77d;
margin: 0.5rem;
border-radius: 4px;
}
.tracker-navigation-button {
display: block;
margin: 4px;
padding-left: 12px;
padding-right: 12px;
border-radius: 4px;
text-align: center;
font-size: 14px;
color: #000;
font-weight: lighter;
}
.tracker-navigation-button:hover {
background-color: #e2eabb !important;
}
.tracker-navigation-button.selected {
background-color: rgb(220, 226, 189);
}
@media all and (max-width: 1700px) {
table.dataTable thead th.upper-row{
position: -webkit-sticky;

View File

@@ -1,150 +0,0 @@
/* CSS Overrides */
.dirt-wrapper{
background-color: #897249;
}
.dirt-wrapper h1{}
.grass-wrapper{
background-color: #3fb24a;
}
.grass-wrapper h1{}
.grassFlowers-wrapper{
background-color: #3fb24a;
}
.grassFlowers-wrapper h1{}
.ice-wrapper{
background-color: #afe0ef;
}
.ice-wrapper h1{}
.jungle-wrapper{
background-color: #2a7808;
}
.jungle-wrapper h1{}
.ocean-wrapper{
background-color: #3667b1;
}
.ocean-wrapper h1{}
.partyTime-wrapper{
background-color: #3a0f69;
color: #ffffff;
}
.partyTime-wrapper h1{}
/* Actual Styles */
h1 {
font-size: 20px;
color: #ffffff;
padding: 5px;
text-align: center;
text-shadow: 1px 1px black;
}
h2 {
padding: 8px;
}
#player-keys-tracker{
width: 600px;
}
#items-container{
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
padding: 5px;
}
#items-container div{
margin: 0;
padding: 0;
}
.image-container{
display: absolute;
height: 75px;
width: 75px;
}
.bottom-text{
position: relative;
align-items: bottom;
text-align: center;
}
.icon{
height: 100%;
position: relative;
left: 15px;
max-width: 45px;
max-height: 45px;
filter: grayscale(100%) contrast(75%) brightness(40%);
}
.icon.acquired{
filter: none;
}
.total-checks{
text-align: center;
padding: 5px;
font-size: 18px;
}
.locations-container{
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 5px;
margin-left: 50px;
margin-right: 50px;
}
.location.acquired{
text-decoration: line-through;
filter: none;
}
.regions-container{
display: flex;
flex-direction: column;
flex-wrap: wrap;
justify-content: space-evenly;
padding: 5px;
text-align: center;
}
.regions-header{
font-size: 18px;
padding: 15px;
cursor: pointer;
text-align: center;
}
.hidden{
display: none;
}
.button-link{
display: block;
width: 100%;
height: 30px;
text-align: center;
text-decoration: none;
line-height: 30px;
background-color: lightgrey;
cursor: pointer;
color: inherit;
}

View File

@@ -157,41 +157,29 @@ html{
background-color: rgba(0, 0, 0, 0.1);
}
#weighted-settings .hints-div{
#weighted-settings .hints-div, #weighted-settings .locations-div{
margin-top: 2rem;
}
#weighted-settings .hints-div h3{
#weighted-settings .hints-div h3, #weighted-settings .locations-div h3{
margin-bottom: 0.5rem;
}
#weighted-settings .hints-div .hints-container{
#weighted-settings .hints-container, #weighted-settings .locations-container{
display: flex;
flex-direction: row;
justify-content: space-between;
}
#weighted-settings .hints-wrapper, #weighted-settings .locations-wrapper{
width: calc(50% - 0.5rem);
font-weight: bold;
}
#weighted-settings .hints-div .hints-wrapper{
width: 32.5%;
}
#weighted-settings .hints-div .hints-wrapper .hint-div{
display: flex;
flex-direction: row;
cursor: pointer;
user-select: none;
-moz-user-select: none;
}
#weighted-settings .hints-div .hints-wrapper .hint-div:hover{
background-color: rgba(0, 0, 0, 0.1);
}
#weighted-settings .hints-div .hints-wrapper .hint-div label{
flex-grow: 1;
padding: 0.125rem 0.5rem;
cursor: pointer;
#weighted-settings .hints-wrapper .simple-list, #weighted-settings .locations-wrapper .simple-list{
margin-top: 0.25rem;
height: 300px;
font-weight: normal;
}
#weighted-settings #weighted-settings-button-row{
@@ -280,6 +268,30 @@ html{
flex-direction: column;
}
#weighted-settings .simple-list{
display: flex;
flex-direction: column;
max-height: 300px;
overflow-y: auto;
border: 1px solid #ffffff;
border-radius: 4px;
}
#weighted-settings .simple-list .list-row label{
display: block;
width: calc(100% - 0.5rem);
padding: 0.0625rem 0.25rem;
}
#weighted-settings .simple-list .list-row label:hover{
background-color: rgba(0, 0, 0, 0.1);
}
#weighted-settings .simple-list .list-row label input[type=checkbox]{
margin-right: 0.5rem;
}
#weighted-settings .invisible{
display: none;
}

View File

@@ -1,14 +1,14 @@
import typing
from collections import Counter, defaultdict
from colorsys import hsv_to_rgb
from datetime import datetime, timedelta, date
from math import tau
import typing
from bokeh.colors import RGB
from bokeh.embed import components
from bokeh.models import HoverTool
from bokeh.plotting import figure, ColumnDataSource
from bokeh.resources import INLINE
from bokeh.colors import RGB
from flask import render_template
from pony.orm import select
@@ -18,10 +18,11 @@ from .models import Room
PLOT_WIDTH = 600
def get_db_data(known_games: str) -> typing.Tuple[typing.Dict[str, int], typing.Dict[datetime.date, typing.Dict[str, int]]]:
def get_db_data(known_games: typing.Set[str]) -> typing.Tuple[typing.Counter[str],
typing.DefaultDict[datetime.date, typing.Dict[str, int]]]:
games_played = defaultdict(Counter)
total_games = Counter()
cutoff = date.today()-timedelta(days=30)
cutoff = date.today() - timedelta(days=30)
room: Room
for room in select(room for room in Room if room.creation_time >= cutoff):
for slot in room.seed.slots:
@@ -93,7 +94,7 @@ def stats():
occurences, legend_label=game, line_width=2, color=game_to_color[game])
total = sum(total_games.values())
pie = figure(plot_height=350, title=f"Games Played in the Last 30 Days (Total: {total})", toolbar_location=None,
pie = figure(title=f"Games Played in the Last 30 Days (Total: {total})", toolbar_location=None,
tools="hover", tooltips=[("Game:", "@games"), ("Played:", "@count")],
sizing_mode="scale_both", width=PLOT_WIDTH, height=500, x_range=(-0.5, 1.2))
pie.axis.visible = False
@@ -121,7 +122,8 @@ def stats():
start_angle="start_angles", end_angle="end_angles", fill_color="colors",
source=ColumnDataSource(data=data), legend_field="games")
per_game_charts = [create_game_played_figure(games_played, game, game_to_color[game]) for game in total_games
per_game_charts = [create_game_played_figure(games_played, game, game_to_color[game]) for game in
sorted(total_games, key=lambda game: total_games[game])
if total_games[game] > 1]
script, charts = components((plot, pie, *per_game_charts))

View File

@@ -1,7 +1,6 @@
{% extends 'pageWrapper.html' %}
{% block head %}
{{ super() }}
<title>Mystery Check Result</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/check.css") }}" />
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/check.js") }}"></script>

Some files were not shown because too many files have changed in this diff Show More