Compare commits

...

109 Commits
0.3.3 ... 0.3.4

Author SHA1 Message Date
Fabian Dill
db5b7e5db9 Core: update version 2022-08-05 14:32:09 +02:00
black-sliver
7c808bb03b SMZ3: Fix Swamp Palace Entrace for minimal accessibility 2022-08-05 14:29:36 +02:00
black-sliver
530b6cc360 SMZ3: FixJunkFillGT making invalid placements 2022-08-05 14:29:22 +02:00
Fabian Dill
95012c004f Subnautica: update docs with resume info (#853)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: strotlog <49286967+strotlog@users.noreply.github.com>
2022-08-05 14:23:21 +02:00
Fabian Dill
59918b9dbc Core: patch stream_input to ignore non-parsable input (such as EOF encoded as 0xff) (#854) 2022-08-03 14:53:14 +02:00
alwaysintreble
b47cca4515 HK: Add bug report link (#824)
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-08-03 14:41:27 +02:00
CaitSith2
5f27019855 Add an optional path to factorio server-settings.json (#851)
* Add an optional path to factorio server-settings.json

* factorio: changes

* use forward slashs in host.yaml going forward.  (works on all OSes.)
* comment out the host.yaml server_settings option.
* assume that server_settings is NOT provided and explicitly check for its existence in factorio_client.
2022-08-01 14:57:30 -07:00
NewSoupVi
0b228834c2 The Witness: Logic fix (unbeatable seed) (#850) 2022-08-01 20:09:34 +02:00
Fabian Dill
57979b9287 WebHost: update flask (#804) 2022-08-01 12:41:15 +02:00
CaitSith2
4b85000960 Fixed a crafting category bug related to fluids. (#848) 2022-07-31 14:01:39 -07:00
Zach Parks
d1f34d088b WebHost: Add links to "Setup Guides" in Supported Games page (#847)
* WebHost: Add links to "Setup Guides" in Supported Games page

* Remove a hanging console.log() I left in
2022-07-31 11:17:26 -04:00
alwaysintreble
3bc9392e5b Core: have generation print plando settings as string instead of numbers (#843)
* have generation print plando settings as string instead of numbers

* Change to __str__

* Make to_string not a class method

* Suggested fix

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

* Fix the fix

* Better quotes

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-07-31 12:02:36 +02:00
lordlou
75165803a0 Sm smz3 create item fix (#844) 2022-07-31 11:08:41 +02:00
lordlou
afc9c772be Sm broken start location fix (#841)
* - fixed basepatches application order breaking (at least) starting location
2022-07-30 18:42:02 +02:00
PoryGone
07450bb83d Migrate DKC3 to APDeltaPatch (#838)
* Add DKC3 to APDeltaPatch

* Undo unintended commit

* More undoing

* Remove else clause
2022-07-29 01:51:22 +02:00
Fabian Dill
2ff7e83ad9 WebHost: make a deeply buried if tree for games a bit more automatic 2022-07-29 01:47:19 +02:00
black-sliver
d817fdcfdb Doc: move Running from source from wiki to docs (#797)
* Doc: move "Running from source" from wiki to docs/

* Doc: update links and reformat running from source

* Doc: implement suggestions in "Running from source"

thanks @alwaysintreble

* Doc: update link to "Running from source"

also link docs/ folder

* Doc: Running from source: Apply suggestions from code review

Co-authored-by: KonoTyran <Kono.Tyran@gmail.com>

Co-authored-by: KonoTyran <Kono.Tyran@gmail.com>
2022-07-29 01:18:59 +02:00
PoryGone
f3d966897f Prevent Krematoa Crash (#832)
* Prevent Krematoa Crash, add crash robustness

* Remove print statements

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

* Consolidate logic for readability
2022-07-29 01:13:00 +02:00
Jarno
9acaf1c279 [Docs] Further explained the mythical InvalidPacket (#828)
* [Docs] Further explained the mythical `InvalidPacket`

* Fixed header category

* Update docs/network protocol.md

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

* Update docs/network protocol.md

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

* Apply suggestions from code review

Co-authored-by: Hussein Farran <hmfarran@gmail.com>

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-07-29 01:11:52 +02:00
NewSoupVi
fd6a0b547f Witness: Fatal logic bug fix (#837)
* Renamed some event items

* Fatal logic bug: Door panels did not check their symbol items
2022-07-28 23:43:35 +02:00
lordlou
c02f355479 Smz3 no progression gt fix (#818) 2022-07-28 12:04:48 +02:00
black-sliver
7d9203ef84 CI: update SNI to 0.0.82 2022-07-28 07:55:53 +02:00
Fabian Dill
e849e4792d WebHost: games played per day plot per game on stats page (#827)
* WebHost: generate stats page palette for maximum hue difference between neighbours.

* WebHost: add per game played stats
2022-07-27 23:36:20 +02:00
Fabian Dill
4565b3af8d DKC3: fix missing default options in Utils.py 2022-07-27 23:34:14 +02:00
Fabian Dill
e5b868e0e9 WebHost: fix 30 days cutoff for stats (#826) 2022-07-27 23:09:40 +02:00
Fabian Dill
489450d3fa SNIClient: fix program not exiting if SNI does not exist nor is running 2022-07-27 22:45:53 +02:00
Fabian Dill
73afab67c8 LttP: fix deprecated use of isSet() (#831) 2022-07-27 22:21:06 +02:00
SoldierofOrder
c61f77029b SC2 docs: Extensive reworks and rewordings. (#809) 2022-07-26 16:53:30 +02:00
Fabian Dill
79702aba65 WebHost: flask caching did a rename 2022-07-26 09:53:18 +02:00
strotlog
1e366ff66f SM: smoother co-op, basepatch internal improvements (#793)
* SM: remote touch instantly, pull ips refactor and symbols

* SM: remove hard-coded ROM address writes

* SM: Full length player table, incl. receive-only player ids

+ apply PR feedback (correct graphic offset, readable data file paths)
2022-07-26 09:43:39 +02:00
Fabian Dill
a0482cf27e Archipidle: Fix forgotten version increment when a new item was added 2022-07-26 09:32:21 +02:00
Ludovic Marechal
288a623ab6 Update ds3 locations and items (#819)
* DS3: Add more rules to avoid softlocks, remove Path of the Dragon gesture location/item and remove useless comments

* DS3: Add more Hostile NPCs locations/items

* DS3: Add missing key items to the key items list
2022-07-26 09:31:16 +02:00
Alchav
3b2037a2d4 HK - focus location (#778) 2022-07-25 22:19:07 +02:00
Fabian Dill
ce536fa3ac Subnautica: fix Multipurpose Room not acquirable in valuable item pool
BaseRoomFragment doesn't exist in vanilla, so when valuable item pool marked it as scannable in vanilla location it did not work, as it's technically BaseRoom
BaseRoom is also required to install other modules into, modules that are already marked as useful, so logically if it's required for other useful stuff it should also be marked as useful
By switching from Fragment to non-fragment one now needs 1 out of 2 instead of 2 out of 2 items, which I consider a plus as well.
2022-07-25 22:17:42 +02:00
PoryGone
41883e44e7 DKC3 - Logic Softlock Fix (#817)
* Add two locations to Trade Sequence List

* Remove trace sequence locations from ROM data dict
2022-07-25 21:34:31 +02:00
Yussur Mustafa Oraji
c3ff201b90 sm64ex: Various Features (#790)
* sm64ex: Course and Secret Randomizer

* sm64ex: Allow higher star door costs, raise minimum amount of stars, deprecate ExtraStars

* sm64ex: Support setting MIPS costs

* sm64ex: Safeguard MIPS Costs
2022-07-25 18:39:31 +02:00
espeon65536
e6635cdd77 OOT updates (#821)
* oot: remove all escape characters in LogicTricks.py

* only attempt to connect to client once

* oot: don't kill player outside ToT or in market entrance
fixed camera makes the game crash outside ToT. added market entrance to be safe, it doesn't matter if you don't die there
2022-07-25 02:07:22 +02:00
NewSoupVi
cfc9d79c79 The Witness: Small changes in response to beta tests (#801)
* Option order and better tooltip

* Logic fix: Hedge Laser requires access to all Hedges

* Add item groups: Lasers, Symbols, Doors

* Update worlds/witness/items.py

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

* Comment for clarity

* Logic fix

* Another logic fix

Co-authored-by: metzner <unconfigured@null.spigotmc.org>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-07-23 12:42:14 +02:00
lordlou
fe2c355739 Sm beam door speedkeep fun accessibility (#785)
added speedkeep option
now forces accessibility to "minimal" instead of (to be deprecated) "item" when "fun" settings is used
2022-07-22 09:44:58 +02:00
alwaysintreble
04c3429839 LttP: Fix scam options (#806) 2022-07-22 00:04:41 -05:00
PoryGone
cabbe0aaf6 Donkey Kong Country 3 Implementation (#798)
* 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
2022-07-22 00:02:25 -05:00
Jolteon0163
a7787d87f9 Add to the ArchipIDLE items list (#807)
* Add to the ArchipIDLE items list

* Update Items.py

* Update Items.py
2022-07-21 18:08:07 -04:00
KonoTyran
79b851189f HK - Fix typos in option names
Fixed max charm and max geo cost display names.
2022-07-21 09:57:10 -07:00
Fabian Dill
9e972eafb2 Subnautica: Add DeathLink (#803) 2022-07-21 08:39:34 -05:00
Fabian Dill
53a995372f Subnautica: add missed PDA 2022-07-21 10:08:19 +02:00
Fabian Dill
17351021b3 Factorio: update rcon lib 2022-07-20 22:29:51 +02:00
Ludovic Marechal
8ff2c1b6f3 DS3: Add the Dark Souls 3 World into Archipelago (#769) 2022-07-20 12:48:14 +02:00
strotlog
45aea2c8ff ChecksFinder: Linux support via wine (#795)
* ChecksFinder: Linux support via wine

* ChecksFinder: account for custom $WINEPREFIX

* ChecksFinder: wine detection
2022-07-19 07:44:04 +02:00
Fabian Dill
9f5e40283a WebHost: reduce server uptime (#794)
* WebHost: attempt to improve wording of server resume
* WebHost: reduce default room timeout to 2 hours


Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-07-18 21:10:29 +02:00
lordlou
025309ec64 SMZ3: Pedestal hint (#792)
* - fixed missing pedestal and tablets hint text for foreign items (was "Don't waste yout time!", is now "A small victory!")

- small precision to SMZ3 and SM docs about "What does another world's item look like in Super Metroid"
2022-07-17 19:40:23 -05:00
NewSoupVi
bd4850b2b5 The Witness 0.3.4 features (#780)
New options:

Shuffle Doors: Many doors in the game will open on their own upon receiving an item ("key").
Variant - Shuffle Door/Control Panels: Many panels in the game that open doors or control devices in the world will be off until receiving their respective item ("key").
Shuffle Lasers: Lasers no longer activate by solving the laser panel, instead you will get an item that activates the laser.
Shuffle Symbols: Now that there is something else to shuffle (doors / door panels), you can turn off Symbol Rando.
Shuffle Postgame (replaces "Shuffle Hard"): The randomizer will now determine by your settings which panels are in the "postgame" - Meaning they can only be accessed after you can complete your win condition anyway.
2022-07-17 12:56:22 +02:00
Fabian Dill
472e114fb9 Final Fantasy: fix outdated advancement flag 2022-07-17 11:19:00 +02:00
espeon65536
828bcb1266 OoT: Fix gerudo_fortress on normal (#784) 2022-07-16 13:00:00 -05:00
t3hf1gm3nt
9897f4eb4b LTTP: Yaml Update (#765)
removes vendor option from hints, adds scam setting, and adds P option to shop shuffle.
2022-07-16 12:56:23 -05:00
Fabian Dill
e1ef820184 Subnautica: add creature scans 2022-07-16 19:54:55 +02:00
lordlou
b3ad766680 SMZ3: Item link support (#756)
* 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

* - enabled local item dialog boxes for dungeon and keycard items when keysanity is used

* - fixed ItemLink support

* fixed shops sending checks

* Added get_filler_item_name() returning a random junk item

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2022-07-16 12:47:26 -05:00
Fabian Dill
74b19dc1f5 WebHost: cleanup generate and hopefully fix SQL concurrency problems 2022-07-16 19:44:29 +02:00
Fabian Dill
449bc93307 Rogue Legacy: obliterate any outdated remnants before installer adds new files 2022-07-16 19:40:59 +02:00
Fabian Dill
622af17705 MultiServer: make !hint prefer non-local 2022-07-16 12:19:24 +02:00
Fabian Dill
a42f7f99fe Factorio: specify rcon version 2022-07-16 01:31:20 +02:00
black-sliver
3c6bd555b4 doc: add style guide (#746)
* doc: add style guide

* doc: style guide for python and markdown

* doc: consistent use of periods and explicit double quotes in style guide

Co-authored-by: Hussein Farran <hmfarran@gmail.com>

* doc: better define string style in style guide

* doc: add format string literals to style guide

* doc: add HTML, CSS and JS to style guide

Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-07-15 23:52:35 +02:00
Rome Reginelli
a4211d5f11 Improve Risk of Rain 2 docs (#770)
* Improve Risk of Rain 2 docs

* RoR2: clarify custom item weight settings

* Update worlds/ror2/docs/en_Risk of Rain 2.md

Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-07-15 17:24:40 -04:00
Vale
090c5bcf00 RoR2: FinalStageDeath (#766)
Added a YAML option for 'FinalStageDeath', a toggle for 'death on the final boss stage counts as a win'. Defaults to on.

Co-authored-by: Vale <58179315+DelosIX@users.noreply.github.com>
2022-07-15 17:21:36 -04:00
alwaysintreble
82850d7f66 Ror2: reduce locations to 250 and mark legendary items as useful (#776)
* reduce total locations to 250

* minor styling cleanup. mark legendary items as useful

* 😡

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-07-15 17:19:36 -04:00
Yussur Mustafa Oraji
86112351a6 sm64ex: Adapt area_connections slotdata Format (#767) 2022-07-15 20:04:26 +02:00
black-sliver
ce789d1e3e SoE: texts, energy core, fragments, useful (#777)
* fix missing fields in custom prog balancing option
* fix typos and pep8
* update and implement pyevermizer 0.41.3
  * allow randomizing energy core
  * add energy core fragments (turn in at Prof. Ruffleberg)
  * rename some items to avoid confusion
  * differentiate between progression and useful
* remove obsolete 'Bazooka' group
* don't add items to the pool that get removed
2022-07-15 18:01:07 +02:00
Fabian Dill
73fb1b8074 Subnautica: updates (#759)
* Subnautica: add more goals

* Subnautica: fix wrongly positioned Databox

* Subnautica: allow techs to remain vanilla

* Subnautica: make zipimport compatible

* Subnautica: force two Seaglide fragments into local sphere 1
2022-07-15 17:41:53 +02:00
black-sliver
8e15fe51b6 Put common options first (#774)
* this applies to yaml and webhost
* this allows overwriting common options from the world
2022-07-15 06:54:29 +02:00
Fabian Dill
aa954b776d MultiServer: add /status and allow status command to dynamically filter for Tags 2022-07-15 02:52:26 +02:00
jsd1982
76f6eb1434 SNIClient: update default SNI port from 8080 to 23074 2022-07-15 02:51:36 +02:00
Yussur Mustafa Oraji
e38308bac3 sm64ex: Allow setting Big Star Door requirements (#773)
* sm64ex: Allow setting Big Star Door requirements

* sm64ex: Lower requirements for StarsToFinish
2022-07-14 18:37:14 +02:00
SoldierofOrder
e804f592de SC2: Windows ".dll missing" fix and fix for finding SC2 install automatically (#721) 2022-07-14 09:51:00 +02:00
Fabian Dill
6e0a0c5c4a Core: skip second sanity check when pushing an item into a location (-O) (#745) 2022-07-14 09:46:03 +02:00
alwaysintreble
122590fc68 lttp: move open pyramid to new options system (#762) 2022-07-14 09:39:53 +02:00
lordlou
c806366469 Sm comeback too strict (#755) 2022-07-14 09:37:45 +02:00
Zach Parks
0d3bd6e2e8 gitignore general Windows/macOS files (#763) 2022-07-10 19:24:07 +00:00
Fabian Dill
beac0b1acd Requirements: update some modules 2022-07-10 19:37:50 +02:00
Bicoloursnake
1cc9c7a469 Doc: Add english mac guide (running from source) (#744)
* Create RunFromSourceGuideForMac.md

* Update and rename RunFromSourceGuideForMac.md to docs/RunFromSourceGuideForMac.md

* Clarified the source code download.

* Rename docs/RunFromSourceGuideForMac.md to worlds/generic/docs/RunFromSourceGuideForMac.md

* Update __init__.py

* Noted the case where a user might want EnemizerCLI

* Updated document to reflect requested changes

Updated to reflect the requested changes as well as including some information on virtual environments.

* Added Capital Letters to SNIClient.py

* Reworked Document Structure

Numeric order of lists now makes sense and changed the virtual environment section to match Archipelago tradition.

* Update __init__.py

* Minor Changes for clarity's sake

* Renamed file to make webhost happy

* Changed mac guide filename
2022-07-10 03:16:41 +02:00
CaitSith2
17db0805a7 Allow potentially all rocket-part ingredients to be fluids. (#753) 2022-07-09 12:35:38 +02:00
CaitSith2
2f53972c85 Factorio: fix accidental removal of fluids from make_balanced_recipe (#754) 2022-07-08 15:35:33 +02:00
black-sliver
9ac780102e Subnautica: display item_pool as Item Pool on the settings page 2022-07-07 21:22:24 +02:00
Fabian Dill
60b80083e0 LttP: fix shop inventory corruption in upgrade fairy 2022-07-07 13:25:17 +02:00
alwaysintreble
8597b04c41 WebHost: Advanced guide cleanup (#725)
* advanced yaml cleanup

* Update advanced_settings_en.md

* i hate this game now

* formatting reverting
2022-07-06 16:06:32 -05:00
Fabian Dill
6a60c46a99 Subnautica: fix generation crash on valuable item pool (#739) 2022-07-06 17:04:22 -04:00
Fabian Dill
5c2163a1a7 WebHost: fix comment typo 2022-07-06 22:32:33 +02:00
Hussein Farran
a49bcd618d Dev Docs: Add SA2B and SC2 to network diagram (#719)
* Add SA2B and SC2 to network diagram

* Remove jpg version of image.

* Fix png of image... Github web editor borked it

* Update network diagram.svg

* We're back to light mode, friends.

Use SVG and JPG that are valid and let you zoom in properly.
2022-07-06 16:12:53 -04:00
Doug Hoskisson
d76b41afe7 RL: Rename Rogue Legacy Folder (#452)
* rename rogue legacy
"`rogue-legacy` is not a valid python module name"

* revert rename of the documentation file
2022-07-06 14:18:28 -05:00
Sunny Bat
ab2b635a77 Update Raft for Final Chapter (#724) 2022-07-06 04:37:08 +02:00
strotlog
7072c7bd45 docs: fix 2 URLs (#738)
* URL of image in Alttp ES tutorial
* Link to RA download in SMZ3 EN tutorial
2022-07-04 10:49:25 +02:00
Alchav
530c5500c3 Break out of fill loop if locations is empty (#690) 2022-07-03 17:11:11 +02:00
Daniel Grace
8870b577d0 Hollow Knight June 2022 Updates (#720)
This is a combined PR for assorted Hollow Knight updates for June 2022 that have cleared testing. It supersedes any HK-exclusive PRs open by myself or @Alchav unless stated otherwise.

Summary of changes below:

 * Implement Split Claw, Split Cloak, Split Superdash, Randomize Nail, Randomize Focus, Randomize Swim and Elevator 
 * Pass options (@Alchav)
 * Add support for Deathlink with three different modes (@dewiniaid)
 * Add customizable additional shop slots per-shop (@Alchav) and overall (@dewiniaid)
 * Overhaul shop cost output to be more generic and account for all locations with standard costs (such as Stag Stations, Cornifer, and Divine) (@dewiniaid)
 * Add "CostSanity", allowing random prices using any cost type to be chosen for any location with a cost. (e.g. a Stag station requiring 15 grubs to obtain an item)
 * Item classification fixes (Map and Journal items are fillter, Mask Shards/Pale Ore/Vessel Fragments are useful) (@Alchav)
 * Fix Ijii -> Jiji (@Alchav )
 * General code quality updates

The above changes are only for the HK world.
2022-07-03 17:10:10 +02:00
Colin Lenzen
7d85ab471a [Timespinner] Rename flag and add tiered loot settings (#699) 2022-07-03 17:05:44 +02:00
Fabian Dill
3205cbf932 Generate: convert plando settings to an IntFlag with error reporting for unknown plando names (#735) 2022-07-03 14:11:52 +02:00
Fabian Dill
b9fb4de878 BaseClasses: make ItemClassification properties faster 2022-07-02 13:56:35 +02:00
Jarno Westhof
bcd7096e1d [The Witness] Update data_version as it was forgotten for 0.3.3
# Conflicts:
#	worlds/witness/docs/setup_en.md
2022-07-02 12:19:08 +02:00
strotlog
b206f2846a SNES games: use JPN as abbreviation for Japan/Japanese 2022-07-02 12:16:15 +02:00
CaitSith2
8a8bc6aa34 Factorio: Fix impossible seeds for rocket-part recipes as well. (#733) 2022-07-01 00:40:31 +02:00
black-sliver
bce7c258c3 CI: update Enemizer to 7.0.1 2022-06-30 22:55:05 +02:00
Fabian Dill
cea7278faf LttP: now that Enemizer allows for AP rom name, rename it. (#730)
* LttP: now that Enemizer allows for AP rom name, rename it.

* LttP: fix missing Enemizer message parenthesis
2022-06-30 10:00:37 -07:00
alwaysintreble
d7a9b98ce8 fix glossary link on sitemap 2022-06-29 22:08:38 +02:00
Alchav
7dcde12e2e Revert SC2 item classifications 2022-06-29 12:15:19 +02:00
black-sliver
ba2a5c4744 MC: add non-windows install to docs (#713)
* MC: add non-windows install to docs

* MC: better link naming for non-windows doc

Co-authored-by: Hussein Farran <hmfarran@gmail.com>

* MC: doc change manual forge link to index

By removing the direct link to the version we avoid having to update it all the time and users will have to check the other version numbers for manual installation anyway.

Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-06-28 19:23:18 +02:00
espeon65536
39ac3c38bf sm64: only apply DDD 100 coin star rule if the location exists (#716) 2022-06-27 23:03:34 -07:00
alwaysintreble
61f751a1db docs: add common terms documentation to website (#680)
* docs: add common terms documentation to website

* minor cleanup

* some rewording and reformatting.

* tighten up world definition clarity

Co-authored-by: Rome Reginelli <mduo13@gmail.com>

* Clarify seed definition a bit better

Co-authored-by: Rome Reginelli <mduo13@gmail.com>

* add text for "out of logic" and that slot names must be unique

* rename common terms to glossary

Co-authored-by: Rome Reginelli <mduo13@gmail.com>
2022-06-27 23:34:47 -04:00
alwaysintreble
5f2193f2e4 ror2: update setup guide (#671)
* ror2: remove yaml template from guide and link to player settings page. Add documentation on chat client

* ror2: copy paste the good config description like everyone else.
2022-06-27 21:05:55 -04:00
Daniel Grace
98b714f84a HK: Add options for Deathlink. (#672) 2022-06-27 21:05:29 -04:00
alwaysintreble
2a0198b618 multiserver: allow !release as an alias for !forfeit (#693)
* multiserver: allow `!release` as an alias for `!forfeit`

* create `/release` command. Add some periods to messages that print in console and point users to release

* Add a missing space on line 1135

Co-authored-by: Chris Wilson <chris@legendserver.info>
2022-06-27 20:59:42 -04:00
The T
cd9f8f3119 SM64: DDD 100 Coins in Entrance Rando should expect sub removal (#711)
I brought this up in #super-mario-64, and the minor consensus is that 100 Coins is "possible", the same way Red Coins is possible.

According to a FAQ online, DDD has 106 coins. That means you are still required to get at least 5 of the red coins in order to get the 100 coin star. If we already have a rule stating the Red Coins require the sub to be removed (by reaching Bowser in the Fire Sea), it should apply to the 100 coins as well.

The consensus on it being "possible" was that it requires a very specific triple jump. There is no "Strict" category for this since it isn't caps/cannons-based, but it is extremely unreasonable to casual play. If you want to sequence break it, go for it, but I don't think it should be expected.
2022-06-27 07:43:48 -05:00
CaitSith2
37b569eca6 Changes: (#639)
* Changes:

* When client loses connection to the server through no fault of your own, it no longer forgets your username.
* It is now possible to do /connect archipelago://username:password@server:port or to paste archipelago://username:password@server:port into the connect bar and hit connect, and have both the username/password filled in that way.

* Switch checksfinder client to getting username from url if suppplied by url.

* Correct the print statement
2022-06-27 03:10:41 -07:00
192 changed files with 14027 additions and 5131 deletions

View File

@@ -17,9 +17,9 @@ jobs:
python-version: '3.8'
- name: Download run-time dependencies
run: |
Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.81/sni-v0.0.81-windows-amd64.zip -OutFile sni.zip
Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-windows-amd64.zip -OutFile sni.zip
Expand-Archive -Path sni.zip -DestinationPath SNI -Force
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/7.0/win-x64.zip -OutFile enemizer.zip
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/7.0.1/win-x64.zip -OutFile enemizer.zip
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
- name: Build
run: |
@@ -63,11 +63,11 @@ jobs:
chmod a+rx appimagetool
- name: Download run-time dependencies
run: |
wget -nv https://github.com/alttpo/sni/releases/download/v0.0.81/sni-v0.0.81-manylinux2014-amd64.tar.xz
wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-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/7.0/ubuntu.16.04-x64.7z
wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0.1/ubuntu.16.04-x64.7z
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
- name: Build
run: |

View File

@@ -51,11 +51,11 @@ jobs:
chmod a+rx appimagetool
- name: Download run-time dependencies
run: |
wget -nv https://github.com/alttpo/sni/releases/download/v0.0.81/sni-v0.0.81-manylinux2014-amd64.tar.xz
wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-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/7.0/ubuntu.16.04-x64.7z
wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0.1/ubuntu.16.04-x64.7z
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
- name: Build
run: |

14
.gitignore vendored
View File

@@ -116,6 +116,9 @@ target/
profile_default/
ipython_config.py
# vim editor
*.swp
# SageMath parsed files
*.sage.py
@@ -152,10 +155,17 @@ dmypy.json
# Cython debug symbols
cython_debug/
#minecraft server stuff
# minecraft server stuff
jdk*/
minecraft*/
minecraft_versions.json
#pyenv
# pyenv
.python-version
# OS General Files
.DS_Store
.AppleDouble
.LSOverride
Thumbs.db
[Dd]esktop.ini

View File

@@ -126,7 +126,6 @@ class MultiWorld():
set_player_attr('beemizer_total_chance', 0)
set_player_attr('beemizer_trap_chance', 0)
set_player_attr('escape_assist', [])
set_player_attr('open_pyramid', False)
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
set_player_attr('treasure_hunt_count', 0)
set_player_attr('clock_mode', False)
@@ -390,20 +389,14 @@ class MultiWorld():
self.state.collect(item, True)
def push_item(self, location: Location, item: Item, collect: bool = True):
if not isinstance(location, Location):
raise RuntimeError(
'Cannot assign item %s to invalid location %s (player %d).' % (item, location, item.player))
assert location.can_fill(self.state, item, False), f"Cannot place {item} into {location}."
location.item = item
item.location = location
item.world = self # try to not have this here anymore and create it with item?
if collect:
self.state.collect(item, location.event, location)
if location.can_fill(self.state, item, False):
location.item = item
item.location = location
item.world = self # try to not have this here anymore
if collect:
self.state.collect(item, location.event, location)
logging.debug('Placed %s at %s', item, location)
else:
raise RuntimeError('Cannot assign item %s to location %s.' % (item, location))
logging.debug('Placed %s at %s', item, location)
def get_entrances(self) -> List[Entrance]:
if self._cached_entrances is None:
@@ -1191,19 +1184,19 @@ class Item:
@property
def advancement(self) -> bool:
return bool(self.classification & ItemClassification.progression)
return ItemClassification.progression in self.classification
@property
def skip_in_prog_balancing(self) -> bool:
return self.classification == ItemClassification.progression_skip_balancing
return ItemClassification.progression_skip_balancing in self.classification
@property
def useful(self) -> bool:
return bool(self.classification & ItemClassification.useful)
return ItemClassification.useful in self.classification
@property
def trap(self) -> bool:
return bool(self.classification & ItemClassification.trap)
return ItemClassification.trap in self.classification
@property
def flags(self) -> int:
@@ -1431,8 +1424,6 @@ class Spoiler():
outfile.write('Entrance Shuffle: %s\n' % self.world.shuffle[player])
if self.world.shuffle[player] != "vanilla":
outfile.write('Entrance Shuffle Seed %s\n' % self.world.worlds[player].er_seed)
outfile.write('Pyramid hole pre-opened: %s\n' % (
'Yes' if self.world.open_pyramid[player] else 'No'))
outfile.write('Shop inventory shuffle: %s\n' %
bool_to_text("i" in self.world.shop_shuffle[player]))
outfile.write('Shop price shuffle: %s\n' %

View File

@@ -1,6 +1,8 @@
from __future__ import annotations
import os
import sys
import asyncio
import shutil
import ModuleUpdate
ModuleUpdate.update()
@@ -32,20 +34,34 @@ class ChecksFinderContext(CommonContext):
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 "localappdata" in os.environ:
self.game_communication_path = os.path.expandvars(r"%localappdata%/ChecksFinder")
else:
# not windows. game is an exe so let's see if wine might be around to run it
if "WINEPREFIX" in os.environ:
wineprefix = os.environ["WINEPREFIX"]
elif shutil.which("wine") or shutil.which("wine-stable"):
wineprefix = os.path.expanduser("~/.wine") # default root of wine system data, deep in which is app data
else:
msg = "ChecksFinderClient couldn't detect system type. Unable to infer required game_communication_path"
logger.error("Error: " + msg)
Utils.messagebox("Error", msg, error=True)
sys.exit(1)
self.game_communication_path = os.path.join(
wineprefix,
"drive_c",
os.path.expandvars("users/$USER/Local Settings/Application Data/ChecksFinder"))
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(ChecksFinderContext, self).server_auth(password_requested)
if not self.auth: # TODO: Replace this if block with await self.getusername() once that PR is merged in.
logger.info('Enter slot name:')
self.auth = await self.console_input()
await self.get_username()
await self.send_connect()
async def connection_closed(self):
await super(ChecksFinderContext, self).connection_closed()
path = os.path.expandvars(r"%localappdata%/ChecksFinder")
for root, dirs, files in os.walk(path):
for root, dirs, files in os.walk(self.game_communication_path):
for file in files:
if file.find("obtain") <= -1:
os.remove(root + "/" + file)
@@ -59,26 +75,25 @@ class ChecksFinderContext(CommonContext):
async def shutdown(self):
await super(ChecksFinderContext, self).shutdown()
path = os.path.expandvars(r"%localappdata%/ChecksFinder")
for root, dirs, files in os.walk(path):
for root, dirs, files in os.walk(self.game_communication_path):
for file in files:
if file.find("obtain") <= -1:
os.remove(root+"/"+file)
def on_package(self, cmd: str, args: dict):
if cmd in {"Connected"}:
if not os.path.exists(os.path.expandvars(r"%localappdata%/ChecksFinder")):
os.mkdir(os.path.expandvars(r"%localappdata%/ChecksFinder"))
if not os.path.exists(self.game_communication_path):
os.makedirs(self.game_communication_path)
for ss in self.checked_locations:
filename = f"send{ss}"
with open(os.path.expandvars(r"%localappdata%/ChecksFinder/" + filename), 'w') as f:
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.close()
if cmd in {"ReceivedItems"}:
start_index = args["index"]
if start_index != len(self.items_received):
for item in args['items']:
filename = f"AP_{str(NetworkItem(*item).location)}PLR{str(NetworkItem(*item).player)}.item"
with open(os.path.expandvars(r"%localappdata%/ChecksFinder/" + filename), 'w') as f:
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.write(str(NetworkItem(*item).item))
f.close()
@@ -86,7 +101,7 @@ class ChecksFinderContext(CommonContext):
if "checked_locations" in args:
for ss in self.checked_locations:
filename = f"send{ss}"
with open(os.path.expandvars(r"%localappdata%/ChecksFinder/" + filename), 'w') as f:
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.close()
def run_gui(self):
@@ -112,10 +127,9 @@ async def game_watcher(ctx: ChecksFinderContext):
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
ctx.syncing = False
path = os.path.expandvars(r"%localappdata%/ChecksFinder")
sending = []
victory = False
for root, dirs, files in os.walk(path):
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]

View File

@@ -43,12 +43,14 @@ 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")
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")
return True
@@ -161,6 +163,7 @@ class CommonContext:
def __init__(self, server_address, password):
# server state
self.server_address = server_address
self.username = None
self.password = password
self.hint_cost = None
self.slot_info = {}
@@ -253,6 +256,13 @@ class CommonContext:
self.password = await self.console_input()
return self.password
async def get_username(self):
if not self.auth:
self.auth = self.username
if not self.auth:
logger.info('Enter slot name:')
self.auth = await self.console_input()
async def send_connect(self, **kwargs):
payload = {
'cmd': 'Connect',
@@ -309,6 +319,7 @@ class CommonContext:
async def shutdown(self):
self.server_address = ""
self.username = None
if self.server and not self.server.socket.closed:
await self.server.socket.close()
if self.server_task:
@@ -469,12 +480,20 @@ async def server_loop(ctx: CommonContext, address=None):
logger.info('Please connect to an Archipelago server.')
return
address = f"ws://{address}" if "://" not in address else address
port = urllib.parse.urlparse(address).port or 38281
address = f"ws://{address}" if "://" not in address \
else address.replace("archipelago://", "ws://")
server_url = urllib.parse.urlparse(address)
if server_url.username:
ctx.username = server_url.username
if server_url.password:
ctx.password = server_url.password
port = server_url.port or 38281
logger.info(f'Connecting to Archipelago server at {address}')
try:
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
ctx.ui.update_address_bar(server_url.netloc)
ctx.server = Endpoint(socket)
logger.info('Connected')
ctx.server_address = address
@@ -585,6 +604,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
raise Exception('Connection refused by the multiworld host, no reason provided')
elif cmd == 'Connected':
ctx.username = ctx.auth
ctx.team = args["team"]
ctx.slot = args["slot"]
# int keys get lost in JSON transfer
@@ -708,10 +728,7 @@ if __name__ == '__main__':
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(TextContext, self).server_auth(password_requested)
if not self.auth:
logger.info('Enter slot name:')
self.auth = await self.console_input()
await self.get_username()
await self.send_connect()
def on_package(self, cmd: str, args: dict):
@@ -722,6 +739,7 @@ if __name__ == '__main__':
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:

View File

@@ -400,6 +400,7 @@ if __name__ == '__main__':
"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()
@@ -410,6 +411,9 @@ if __name__ == '__main__':
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.")
@@ -421,7 +425,10 @@ if __name__ == '__main__':
else:
raise FileNotFoundError(f"Path {executable} is not an executable file.")
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest)
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()

12
Fill.py
View File

@@ -42,8 +42,16 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
for item_to_place in items_to_place:
while items_to_place:
# if we have run out of locations to fill,break out of this loop
if not locations:
unplaced_items += items_to_place
break
item_to_place = items_to_place.pop(0)
spot_to_fill: typing.Optional[Location] = None
# if minimal accessibility, only check whether location is reachable if game not beatable
if world.accessibility[item_to_place.player] == 'minimal':
perform_access_check = not world.has_beaten_game(maximum_exploration_state,
item_to_place.player) \
@@ -54,7 +62,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
for i, location in enumerate(locations):
if (not single_player_placement or location.player == item_to_place.player) \
and location.can_fill(maximum_exploration_state, item_to_place, perform_access_check):
# poping by index is faster than removing by content,
# popping by index is faster than removing by content,
spot_to_fill = locations.pop(i)
# skipping a scan for the element
break

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
import argparse
import logging
import random
@@ -7,6 +9,7 @@ from typing import Set, Dict, Tuple, Callable, Any, Union
import os
from collections import Counter
import string
import enum
import ModuleUpdate
@@ -25,7 +28,43 @@ from worlds.alttp.Text import TextTable
from worlds.AutoWorld import AutoWorldRegister
import copy
categories = set(AutoWorldRegister.world_types)
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():
@@ -64,7 +103,7 @@ def mystery_argparse():
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: Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
args.plando: PlandoSettings = PlandoSettings.from_option_string(args.plando)
return args, options
@@ -127,7 +166,7 @@ def main(args=None, callback=ERmain):
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"{', '.join(args.plando)}")
f"{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. "
@@ -403,7 +442,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
def get_plando_bosses(boss_shuffle: str, plando_options: Set[str]) -> str:
if boss_shuffle in boss_shuffle_options:
return boss_shuffle_options[boss_shuffle]
elif "bosses" in plando_options:
elif PlandoSettings.bosses in plando_options:
options = boss_shuffle.lower().split(";")
remainder_shuffle = "none" # vanilla
bosses = []
@@ -452,7 +491,7 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
setattr(ret, option_key, option(option.default))
def roll_settings(weights: dict, plando_options: Set[str] = frozenset(("bosses",))):
def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings.bosses):
if "linked_options" in weights:
weights = roll_linked_options(weights)
@@ -465,17 +504,11 @@ def roll_settings(weights: dict, plando_options: Set[str] = frozenset(("bosses",
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 = requirements.get("plando", "")
if required_plando_options:
required_plando_options = set(option.strip() for option in required_plando_options.split(","))
required_plando_options -= plando_options
required_plando_options = PlandoSettings.from_option_string(requirements.get("plando", ""))
if required_plando_options not in plando_options:
if required_plando_options:
if len(required_plando_options) == 1:
raise Exception(f"Settings reports required plando module {', '.join(required_plando_options)}, "
f"which is not enabled.")
else:
raise Exception(f"Settings reports required plando modules {', '.join(required_plando_options)}, "
f"which are not enabled.")
raise Exception(f"Settings reports required plando module {str(required_plando_options)}, "
f"which is not enabled.")
ret = argparse.Namespace()
for option_key in Options.per_game_common_options:
@@ -504,12 +537,12 @@ def roll_settings(weights: dict, plando_options: Set[str] = frozenset(("bosses",
# 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)
if "items" in 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 "connections" in plando_options:
if PlandoSettings.connections in plando_options:
options = game_weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
@@ -555,9 +588,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.goal = goals[goal]
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
# fast ganon + ganon at hole
ret.open_pyramid = get_choice_legacy('open_pyramid', weights, 'goal')
extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available')
@@ -629,7 +659,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 "texts" in plando_options:
if PlandoSettings.texts in plando_options:
tt = TextTable()
tt.removeUnwantedText()
options = weights.get("plando_texts", [])
@@ -641,7 +671,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 "connections" in plando_options:
if PlandoSettings.connections in plando_options:
options = weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice_legacy("percentage", placement, 100)):

View File

@@ -126,7 +126,7 @@ components: Iterable[Component] = (
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'),
# SNI
Component('SNI Client', 'SNIClient',
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3')),
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3')),
Component('LttP Adjuster', 'LttPAdjuster'),
# Factorio
Component('Factorio Client', 'FactorioClient'),

View File

@@ -47,7 +47,7 @@ def main():
parser.add_argument('rom', nargs="?", default='AP_LttP.sfc', help='Path to an ALttP rom to adjust.')
parser.add_argument('--baserom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc',
help='Path to an ALttP JAP(1.0) rom to use as a base.')
help='Path to an ALttP Japan(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default='info', const='info', nargs='?',
choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--menuspeed', default='normal', const='normal', nargs='?',
@@ -289,7 +289,7 @@ def run_sprite_update():
else:
top.withdraw()
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
while not done.isSet():
while not done.is_set():
task.do_events()
logging.info("Done updating sprites")
@@ -300,6 +300,7 @@ def update_sprites(task, on_finish=None):
sprite_dir = user_path("data", "sprites", "alttpr")
os.makedirs(sprite_dir, exist_ok=True)
ctx = get_cert_none_ssl_context()
def finished():
task.close_window()
if on_finish:
@@ -1263,4 +1264,4 @@ class ToolTips(object):
if __name__ == '__main__':
main()
main()

View File

@@ -47,7 +47,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world.item_functionality = args.item_functionality.copy()
world.timer = args.timer.copy()
world.goal = args.goal.copy()
world.open_pyramid = args.open_pyramid.copy()
world.boss_shuffle = args.shufflebosses.copy()
world.enemy_health = args.enemy_health.copy()
world.enemy_damage = args.enemy_damage.copy()
@@ -364,7 +363,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for location in world.get_filled_locations():
if type(location.address) == int:
assert location.item.code is not None, "item code None should be event, " \
"location.address should then also be None"
"location.address should then also be None. Location: " \
f" {location}"
locations_data[location.player][location.address] = \
location.item.code, location.item.player, location.item.flags
if location.name in world.start_location_hints[location.player]:

View File

@@ -720,16 +720,16 @@ def get_players_string(ctx: Context):
return f'{len(auth_clients)} players of {total} connected ' + text[:-1]
def get_status_string(ctx: Context, team: int):
text = "Player Status on your team:"
def get_status_string(ctx: Context, team: int, tag: str):
text = f"Player Status on team {team}:"
for slot in ctx.locations:
connected = len(ctx.clients[team][slot])
death_link = len([client for client in ctx.clients[team][slot] if "DeathLink" in client.tags])
tagged = len([client for client in ctx.clients[team][slot] if tag in client.tags])
completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})"
death_text = f" {death_link} of which are death link" if connected else ""
tag_text = f" {tagged} of which are tagged {tag}" if connected and tag else ""
goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "."
text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \
f"{death_text}{goal_text} {completion_text}"
f"{tag_text}{goal_text} {completion_text}"
return text
@@ -766,7 +766,7 @@ def update_checked_locations(ctx: Context, team: int, slot: int):
def forfeit_player(ctx: Context, team: int, slot: int):
"""register any locations that are in the multidata"""
all_locations = set(ctx.locations[slot])
ctx.notify_all("%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
ctx.notify_all("%s (Team #%d) has released all remaining items from their world." % (ctx.player_names[(team, slot)], team + 1))
register_location_checks(ctx, team, slot, all_locations)
update_checked_locations(ctx, team, slot)
@@ -779,7 +779,7 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False):
if values[1] == slot:
all_locations[source_slot].add(location_id)
ctx.notify_all("%s (Team #%d) has collected" % (ctx.player_names[(team, slot)], team + 1))
ctx.notify_all("%s (Team #%d) has collected their items from other worlds." % (ctx.player_names[(team, slot)], team + 1))
for source_player, location_ids in all_locations.items():
register_location_checks(ctx, team, source_player, location_ids, count_activity=False)
update_checked_locations(ctx, team, source_player)
@@ -1106,20 +1106,26 @@ class ClientMessageProcessor(CommonCommandProcessor):
return self.ctx.commandprocessor(command)
def _cmd_players(self) -> bool:
"""Get information about connected and missing players"""
"""Get information about connected and missing players."""
if len(self.ctx.player_names) < 10:
self.ctx.notify_all(get_players_string(self.ctx))
else:
self.output(get_players_string(self.ctx))
return True
def _cmd_status(self) -> bool:
"""Get status information about your team."""
self.output(get_status_string(self.ctx, self.client.team))
def _cmd_status(self, tag:str="") -> bool:
"""Get status information about your team.
Optionally mention a Tag name and get information on who has that Tag.
For example: DeathLink or EnergyLink."""
self.output(get_status_string(self.ctx, self.client.team, tag))
return True
def _cmd_release(self) -> bool:
"""Sends remaining items in your world to their recipients."""
return self._cmd_forfeit()
def _cmd_forfeit(self) -> bool:
"""Surrender and send your remaining items out to their recipients"""
"""Surrender and send your remaining items out to their recipients. Use release in the future."""
if self.ctx.allow_forfeits.get((self.client.team, self.client.slot), False):
forfeit_player(self.ctx, self.client.team, self.client.slot)
return True
@@ -1128,7 +1134,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
return True
elif "disabled" in self.ctx.forfeit_mode:
self.output(
"Sorry, client forfeiting has been disabled on this server. You can ask the server admin for a /forfeit")
"Sorry, client item releasing has been disabled on this server. You can ask the server admin for a /release")
return False
else: # is auto or goal
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
@@ -1136,8 +1142,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
return True
else:
self.output(
"Sorry, client forfeiting requires you to have beaten the game on this server."
" You can ask the server admin for a /forfeit")
"Sorry, client item releasing requires you to have beaten the game on this server."
" You can ask the server admin for a /release")
return False
def _cmd_collect(self) -> bool:
@@ -1302,6 +1308,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
can_pay = 1000
self.ctx.random.shuffle(not_found_hints)
# By popular vote, make hints prefer non-local placements
not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player))
hints = found_hints
while can_pay > 0:
@@ -1653,6 +1661,14 @@ class ServerCommandProcessor(CommonCommandProcessor):
self.output(get_players_string(self.ctx))
return True
def _cmd_status(self, tag: str = "") -> bool:
"""Get status information about teams.
Optionally mention a Tag name and get information on who has that Tag.
For example: DeathLink or EnergyLink."""
for team in self.ctx.clients:
self.output(get_status_string(self.ctx, team, tag))
return True
def _cmd_exit(self) -> bool:
"""Shutdown the server"""
asyncio.create_task(self.ctx.server.ws_server._close())
@@ -1698,43 +1714,48 @@ class ServerCommandProcessor(CommonCommandProcessor):
self.output(f"Could not find player {player_name} to collect")
return False
@mark_raw
def _cmd_release(self, player_name: str) -> bool:
"""Send out the remaining items from a player to their intended recipients."""
return self._cmd_forfeit(player_name)
@mark_raw
def _cmd_forfeit(self, player_name: str) -> bool:
"""Send out the remaining items from a player to their intended recipients"""
"""Send out the remaining items from a player to their intended recipients."""
seeked_player = player_name.lower()
for (team, slot), name in self.ctx.player_names.items():
if name.lower() == seeked_player:
forfeit_player(self.ctx, team, slot)
return True
self.output(f"Could not find player {player_name} to forfeit")
self.output(f"Could not find player {player_name} to release")
return False
@mark_raw
def _cmd_allow_forfeit(self, player_name: str) -> bool:
"""Allow the specified player to use the !forfeit command"""
"""Allow the specified player to use the !release command."""
seeked_player = player_name.lower()
for (team, slot), name in self.ctx.player_names.items():
if name.lower() == seeked_player:
self.ctx.allow_forfeits[(team, slot)] = True
self.output(f"Player {player_name} is now allowed to use the !forfeit command at any time.")
self.output(f"Player {player_name} is now allowed to use the !release command at any time.")
return True
self.output(f"Could not find player {player_name} to allow the !forfeit command for.")
self.output(f"Could not find player {player_name} to allow the !release command for.")
return False
@mark_raw
def _cmd_forbid_forfeit(self, player_name: str) -> bool:
""""Disallow the specified player from using the !forfeit command"""
""""Disallow the specified player from using the !release command."""
seeked_player = player_name.lower()
for (team, slot), name in self.ctx.player_names.items():
if name.lower() == seeked_player:
self.ctx.allow_forfeits[(team, slot)] = False
self.output(
f"Player {player_name} has to follow the server restrictions on use of the !forfeit command.")
f"Player {player_name} has to follow the server restrictions on use of the !release command.")
return True
self.output(f"Could not find player {player_name} to forbid the !forfeit command for.")
self.output(f"Could not find player {player_name} to forbid the !release command for.")
return False
def _cmd_send_multiple(self, amount: typing.Union[int, str], player_name: str, *item_name: str) -> bool:

View File

@@ -48,7 +48,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 = 1
script_version: int = 2
def get_item_value(ap_id):
return ap_id - 66000
@@ -186,7 +186,7 @@ async def n64_sync_task(ctx: OoTContext):
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 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))

View File

@@ -166,27 +166,31 @@ GAME_ALTTP = "A Link to the Past"
GAME_SM = "Super Metroid"
GAME_SOE = "Secret of Evermore"
GAME_SMZ3 = "SMZ3"
supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore", "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_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 JAP10HASH as HASH
from worlds.alttp.Rom import LTTPJPN10HASH as HASH
elif game == GAME_SM:
from worlds.sm.Rom import JAP10HASH as HASH
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 JAP10HASH as ALTTPHASH
from worlds.sm.Rom import JAP10HASH as SMHASH
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.")
@@ -216,7 +220,10 @@ def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str
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 ".apm3")
".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
@@ -245,6 +252,8 @@ def get_base_rom_data(game: str):
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()
@@ -389,6 +398,13 @@ if __name__ == "__main__":
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}")
@@ -396,7 +412,9 @@ if __name__ == "__main__":
def _handle_zip_file_entry(zfinfo: zipfile.ZipInfo, server: str):
data = zfr.read(zfinfo)
if zfinfo.filename.endswith(".apbp") or zfinfo.filename.endswith(".apm3"):
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)

View File

@@ -26,6 +26,8 @@ Currently, the following games are supported:
* The Witness
* Sonic Adventure 2: Battle
* Starcraft 2: Wings of Liberty
* Donkey Kong Country 3
* Dark Souls 3
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
@@ -49,7 +51,7 @@ Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEnt
## Running Archipelago
For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only.
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our wiki page on [running Archipelago from source](https://github.com/ArchipelagoMW/Archipelago/wiki/Running-from-source).
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our doc on [running Archipelago from source](docs/running%20from%20source.md).
## Related Repositories
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present.
@@ -66,7 +68,7 @@ Contributions are welcome. We have a few asks of any new contributors.
Otherwise, we tend to judge code on a case to case basis. It is a generally good idea to stick to PEP-8 guidelines to ensure consistency with existing code. (And to make the linter happy.)
For adding a new game to Archipelago and other documentation on how Archipelago functions, please see the docs folder for the relevant information and feel free to ask any questions in the #archipelago-dev channel in our discord.
For adding a new game to Archipelago and other documentation on how Archipelago functions, please see [the docs folder](docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev channel in our discord.
## FAQ
For frequently asked questions see the website's [FAQ Page](https://archipelago.gg/faq/en/)

View File

@@ -33,7 +33,7 @@ from worlds.sm.Rom import ROM_PLAYER_LIMIT as SM_ROM_PLAYER_LIMIT
from worlds.smz3.Rom import ROM_PLAYER_LIMIT as SMZ3_ROM_PLAYER_LIMIT
import Utils
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
from Patch import GAME_ALTTP, GAME_SM, GAME_SMZ3
from Patch import GAME_ALTTP, GAME_SM, GAME_SMZ3, GAME_DKC3
snes_logger = logging.getLogger("SNES")
@@ -62,7 +62,7 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
def _cmd_snes(self, snes_options: str = "") -> bool:
"""Connect to a snes. Optionally include network address of a snes to connect to,
otherwise show available devices; and a SNES device number if more than one SNES is detected.
Examples: "/snes", "/snes 1", "/snes localhost:8080 1" """
Examples: "/snes", "/snes 1", "/snes localhost:23074 1" """
snes_address = self.ctx.snes_address
snes_device_number = -1
@@ -188,7 +188,10 @@ class Context(CommonContext):
async def shutdown(self):
await super(Context, self).shutdown()
if self.snes_connect_task:
await self.snes_connect_task
try:
await asyncio.wait_for(self.snes_connect_task, 1)
except asyncio.TimeoutError:
self.snes_connect_task.cancel()
def on_package(self, cmd: str, args: dict):
if cmd in {"Connected", "RoomUpdate"}:
@@ -251,6 +254,9 @@ async def deathlink_kill_player(ctx: Context):
if not gamemode or gamemode[0] in SM_DEATH_MODES or (
ctx.death_link_allow_survive and health is not None and health > 0):
ctx.death_state = DeathState.dead
elif ctx.game == GAME_DKC3:
from worlds.dkc3.Client import deathlink_kill_player as dkc3_deathlink_kill_player
await dkc3_deathlink_kill_player(ctx)
ctx.last_death_link = time.time()
@@ -595,7 +601,7 @@ class SNESState(enum.IntEnum):
SNES_ATTACHED = 3
def launch_sni(ctx: Context):
def launch_sni():
sni_path = Utils.get_options()["lttp_options"]["sni"]
if not os.path.isdir(sni_path):
@@ -633,11 +639,9 @@ async def _snes_connect(ctx: Context, address: str):
address = f"ws://{address}" if "://" not in address else address
snes_logger.info("Connecting to SNI at %s ..." % address)
seen_problems = set()
succesful = False
while not succesful:
while 1:
try:
snes_socket = await websockets.connect(address, ping_timeout=None, ping_interval=None)
succesful = True
except Exception as e:
problem = "%s" % e
# only tell the user about new problems, otherwise silently lay in wait for a working connection
@@ -647,7 +651,7 @@ async def _snes_connect(ctx: Context, address: str):
if len(seen_problems) == 1:
# this is the first problem. Let's try launching SNI if it isn't already running
launch_sni(ctx)
launch_sni()
await asyncio.sleep(1)
else:
@@ -1034,44 +1038,48 @@ async def game_watcher(ctx: Context):
if not ctx.rom:
ctx.finished_game = False
ctx.death_link_allow_survive = False
game_name = await snes_read(ctx, SM_ROMNAME_START, 5)
if game_name is None:
continue
elif game_name[:2] == b"SM":
ctx.game = GAME_SM
# versions lower than 0.3.0 dont have item handling flag nor remote item support
romVersion = int(game_name[2:5].decode('UTF-8'))
if romVersion < 30:
ctx.items_handling = 0b001 # full local
else:
item_handling = await snes_read(ctx, SM_REMOTE_ITEM_FLAG_ADDR, 1)
ctx.items_handling = 0b001 if item_handling is None else item_handling[0]
else:
game_name = await snes_read(ctx, SMZ3_ROMNAME_START, 3)
if game_name == b"ZSM":
ctx.game = GAME_SMZ3
ctx.items_handling = 0b101 # local items and remote start inventory
else:
ctx.game = GAME_ALTTP
ctx.items_handling = 0b001 # full local
rom = await snes_read(ctx, SM_ROMNAME_START if ctx.game == GAME_SM else SMZ3_ROMNAME_START if ctx.game == GAME_SMZ3 else ROMNAME_START, ROMNAME_SIZE)
if rom is None or rom == bytes([0] * ROMNAME_SIZE):
continue
from worlds.dkc3.Client import dkc3_rom_init
init_handled = await dkc3_rom_init(ctx)
if not init_handled:
game_name = await snes_read(ctx, SM_ROMNAME_START, 5)
if game_name is None:
continue
elif game_name[:2] == b"SM":
ctx.game = GAME_SM
# versions lower than 0.3.0 dont have item handling flag nor remote item support
romVersion = int(game_name[2:5].decode('UTF-8'))
if romVersion < 30:
ctx.items_handling = 0b001 # full local
else:
item_handling = await snes_read(ctx, SM_REMOTE_ITEM_FLAG_ADDR, 1)
ctx.items_handling = 0b001 if item_handling is None else item_handling[0]
else:
game_name = await snes_read(ctx, SMZ3_ROMNAME_START, 3)
if game_name == b"ZSM":
ctx.game = GAME_SMZ3
ctx.items_handling = 0b101 # local items and remote start inventory
else:
ctx.game = GAME_ALTTP
ctx.items_handling = 0b001 # full local
ctx.rom = rom
if ctx.game != GAME_SMZ3:
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else
SM_DEATH_LINK_ACTIVE_ADDR, 1)
if death_link:
ctx.allow_collect = bool(death_link[0] & 0b100)
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
await ctx.update_death_link(bool(death_link[0] & 0b1))
if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
ctx.locations_checked = set()
ctx.locations_scouted = set()
ctx.locations_info = {}
ctx.prev_rom = ctx.rom
rom = await snes_read(ctx, SM_ROMNAME_START if ctx.game == GAME_SM else SMZ3_ROMNAME_START if ctx.game == GAME_SMZ3 else ROMNAME_START, ROMNAME_SIZE)
if rom is None or rom == bytes([0] * ROMNAME_SIZE):
continue
ctx.rom = rom
if ctx.game != GAME_SMZ3:
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else
SM_DEATH_LINK_ACTIVE_ADDR, 1)
if death_link:
ctx.allow_collect = bool(death_link[0] & 0b100)
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
await ctx.update_death_link(bool(death_link[0] & 0b1))
if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
ctx.locations_checked = set()
ctx.locations_scouted = set()
ctx.locations_info = {}
ctx.prev_rom = ctx.rom
if ctx.awaiting_rom:
await ctx.server_auth(False)
@@ -1279,6 +1287,9 @@ async def game_watcher(ctx: Context):
color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
ctx.location_names[item.location], itemOutPtr, len(ctx.items_received)))
await snes_flush_writes(ctx)
elif ctx.game == GAME_DKC3:
from worlds.dkc3.Client import dkc3_game_watcher
await dkc3_game_watcher(ctx)
async def run_game(romfile):
@@ -1296,7 +1307,7 @@ async def main():
parser = get_base_parser()
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a Archipelago Binary Patch file')
parser.add_argument('--snes', default='localhost:8080', help='Address of the SNI server.')
parser.add_argument('--snes', default='localhost:23074', help='Address of the SNI server.')
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
args = parser.parse_args()

View File

@@ -19,7 +19,13 @@ from worlds.sc2wol.Items import lookup_id_to_name, item_table
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
from worlds.sc2wol import SC2WoLWorld
from Utils import init_logging
from pathlib import Path
import re
from MultiServer import mark_raw
import ctypes
import sys
from Utils import init_logging, is_windows
if __name__ == "__main__":
init_logging("SC2Client", exception_logger="Client")
@@ -73,6 +79,17 @@ class StarcraftClientProcessor(ClientCommandProcessor):
request_unfinished_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui, self.ctx)
return True
@mark_raw
def _cmd_set_path(self, path: str = '') -> bool:
"""Manually set the SC2 install directory (if the automatic detection fails)."""
if path:
os.environ["SC2PATH"] = path
check_mod_install()
return True
else:
sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.")
return False
class SC2Context(CommonContext):
command_processor = StarcraftClientProcessor
@@ -95,10 +112,7 @@ class SC2Context(CommonContext):
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(SC2Context, self).server_auth(password_requested)
if not self.auth:
logger.info('Enter slot name:')
self.auth = await self.console_input()
await self.get_username()
await self.send_connect()
def on_package(self, cmd: str, args: dict):
@@ -114,6 +128,11 @@ class SC2Context(CommonContext):
for mission in slot_req_table:
self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission])
# Look for and set SC2PATH.
# check_game_install_path() returns True if and only if it finds + sets SC2PATH.
if "SC2PATH" not in os.environ and check_game_install_path():
check_mod_install()
if cmd in {"PrintJSON"}:
if "receiving" in args:
if self.slot_concerns_self(args["receiving"]):
@@ -418,8 +437,9 @@ async def starcraft_launch(ctx: SC2Context, mission_id):
sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.")
run_game(sc2.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id),
name="Archipelago", fullscreen=True)], realtime=True)
with DllDirectory(None):
run_game(sc2.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):
@@ -799,6 +819,101 @@ def initialize_blank_mission_dict(location_table):
return unlocks
def check_game_install_path() -> bool:
# First thing: go to the default location for ExecuteInfo.
# An exception for Windows is included because it's very difficult to find ~\Documents if the user moved it.
if is_windows:
# The next five lines of utterly inscrutable code are brought to you by copy-paste from Stack Overflow.
# https://stackoverflow.com/questions/6227590/finding-the-users-my-documents-path/30924555#
import ctypes.wintypes
CSIDL_PERSONAL = 5 # My Documents
SHGFP_TYPE_CURRENT = 0 # Get current, not default value
buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf)
documentspath = buf.value
einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt"))
else:
einfo = str(sc2.paths.get_home() / Path(sc2.paths.USERPATH[sc2.paths.PF]))
# Check if the file exists.
if os.path.isfile(einfo):
# Open the file and read it, picking out the latest executable's path.
with open(einfo) as f:
content = f.read()
if content:
base = re.search(r" = (.*)Versions", content).group(1)
if os.path.exists(base):
executable = sc2.paths.latest_executeble(Path(base).expanduser() / "Versions")
# Finally, check the path for an actual executable.
# If we find one, great. Set up the SC2PATH.
if os.path.isfile(executable):
sc2_logger.info(f"Found an SC2 install at {base}!")
sc2_logger.debug(f"Latest executable at {executable}.")
os.environ["SC2PATH"] = base
sc2_logger.debug(f"SC2PATH set to {base}.")
return True
else:
sc2_logger.warning(f"We may have found an SC2 install at {base}, but couldn't find {executable}.")
else:
sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.")
else:
sc2_logger.warning(f"Couldn't find {einfo}. Please run /set_path with your SC2 install directory.")
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
class DllDirectory:
# Credit to Black Sliver for this code.
# More info: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw
_old: typing.Optional[str] = None
_new: typing.Optional[str] = None
def __init__(self, new: typing.Optional[str]):
self._new = new
def __enter__(self):
old = self.get()
if self.set(self._new):
self._old = old
def __exit__(self, *args):
if self._old is not None:
self.set(self._old)
@staticmethod
def get() -> str:
if sys.platform == "win32":
n = ctypes.windll.kernel32.GetDllDirectoryW(0, None)
buf = ctypes.create_unicode_buffer(n)
ctypes.windll.kernel32.GetDllDirectoryW(n, buf)
return buf.value
# NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
return None
@staticmethod
def set(s: typing.Optional[str]) -> bool:
if sys.platform == "win32":
return ctypes.windll.kernel32.SetDllDirectoryW(s) != 0
# NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific
return False
if __name__ == '__main__':
colorama.init()
asyncio.run(main())

View File

@@ -30,7 +30,7 @@ class Version(typing.NamedTuple):
build: int
__version__ = "0.3.3"
__version__ = "0.3.4"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith('linux')
@@ -277,7 +277,12 @@ def get_default_options() -> dict:
},
"oot_options": {
"rom_file": "The Legend of Zelda - Ocarina of Time.z64",
}
},
"dkc3_options": {
"rom_file": "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc",
"sni": "SNI",
"rom_start": True,
},
}
return options
@@ -474,9 +479,13 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
def stream_input(stream, queue):
def queuer():
while 1:
text = stream.readline().strip()
if text:
queue.put_nowait(text)
try:
text = stream.readline().strip()
except UnicodeDecodeError as e:
logging.exception(e)
else:
if text:
queue.put_nowait(text)
from threading import Thread
thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True)

View File

@@ -69,6 +69,10 @@ class B64UUIDConverter(BaseConverter):
app.url_map.converters["suuid"] = B64UUIDConverter
app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
# has automatic patch integration
import Patch
app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types
def get_world_theme(game_name: str):
if game_name in AutoWorldRegister.world_types:
@@ -141,6 +145,11 @@ def faq(lang):
return render_template("faq.html", lang=lang)
@app.route('/glossary/<string:lang>/')
def terms(lang):
return render_template("glossary.html", lang=lang)
@app.route('/seed/<suuid:seed>')
def view_seed(seed: UUID):
seed = Seed.get(id=seed)

View File

@@ -154,8 +154,10 @@ def autogen(config: dict):
while 1:
time.sleep(0.1)
with db_session:
# for update locks the database row(s) during transaction, preventing writes from elsewhere
to_start = select(
generation for generation in Generation if generation.state == STATE_QUEUED)
generation for generation in Generation
if generation.state == STATE_QUEUED).for_update()
for generation in to_start:
launch_generator(generator_pool, generation)
except AlreadyRunningException:

View File

@@ -12,7 +12,7 @@ def allowed_file(filename):
return filename.endswith(('.txt', ".yaml", ".zip"))
from Generate import roll_settings
from Generate import roll_settings, PlandoSettings
from Utils import parse_yamls
@@ -65,7 +65,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 = set(plando_options)
plando_options = PlandoSettings.from_set(set(plando_options))
results = {}
rolled_results = {}
for filename, text in options.items():

View File

@@ -78,6 +78,8 @@ def download_slot_file(room_id, player_id: int):
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apv6"
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"
else:
return "Game download not supported."
return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname)

View File

@@ -4,7 +4,7 @@ import random
import json
import zipfile
from collections import Counter
from typing import Dict, Optional as TypeOptional
from typing import Dict, Optional, Any
from Utils import __version__
from flask import request, flash, redirect, url_for, session, render_template
@@ -12,10 +12,10 @@ from flask import request, flash, redirect, url_for, session, render_template
from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain
from BaseClasses import seeddigits, get_seed
from Generate import handle_name
from Generate import handle_name, PlandoSettings
import pickle
from .models import *
from .models import Generation, STATE_ERROR, STATE_QUEUED, commit, db_session, Seed, UUID
from WebHostLib import app
from .check import get_yaml_data, roll_options
from .upload import upload_zip_to_db
@@ -30,16 +30,15 @@ def get_meta(options_source: dict) -> dict:
}
plando_options -= {""}
meta = {
server_options = {
"hint_cost": int(options_source.get("hint_cost", 10)),
"forfeit_mode": options_source.get("forfeit_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),
"plando_options": list(plando_options)
}
return meta
return {"server_options": server_options, "plando_options": list(plando_options)}
@app.route('/generate', methods=['GET', 'POST'])
@@ -60,13 +59,13 @@ def generate(race=False):
results, gen_options = roll_options(options, meta["plando_options"])
if race:
meta["item_cheat"] = False
meta["remaining_mode"] = "disabled"
meta["server_options"]["item_cheat"] = False
meta["server_options"]["remaining_mode"] = "disabled"
if any(type(result) == str for result in results.values()):
return render_template("checkResult.html", results=results)
elif len(gen_options) > app.config["MAX_ROLL"]:
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players for now. "
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
f"If you have a larger group, please generate it yourself and upload it.")
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
gen = Generation(
@@ -92,35 +91,35 @@ def generate(race=False):
return render_template("generate.html", race=race, version=__version__)
def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=None, sid=None):
def gen_game(gen_options, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None):
if not meta:
meta: Dict[str, object] = {}
meta: Dict[str, Any] = {}
meta.setdefault("server_options", {}).setdefault("hint_cost", 10)
race = meta.setdefault("race", False)
meta.setdefault("hint_cost", 10)
race = meta.get("race", False)
del (meta["race"])
plando_options = meta.get("plando", {"bosses", "items", "connections", "texts"})
del (meta["plando_options"])
try:
target = tempfile.TemporaryDirectory()
playercount = len(gen_options)
seed = get_seed()
random.seed(seed)
if race:
random.seed() # reset to time-based random source
random.seed() # use time-based random source
else:
random.seed(seed)
seedname = "W" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
erargs = parse_arguments(['--multi', str(playercount)])
erargs.seed = seed
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwrittin in mystery
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.race = race
erargs.outputname = seedname
erargs.outputpath = target.name
erargs.teams = 1
erargs.plando_options = ", ".join(plando_options)
erargs.plando_options = PlandoSettings.from_set(meta.setdefault("plando_options",
{"bosses", "items", "connections", "texts"}))
name_counter = Counter()
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
@@ -136,7 +135,7 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
if len(set(erargs.name.values())) != len(erargs.name):
raise Exception(f"Names have to be unique. Names: {Counter(erargs.name.values())}")
ERmain(erargs, seed, baked_server_options=meta)
ERmain(erargs, seed, baked_server_options=meta["server_options"])
return upload_to_db(target.name, sid, owner, race)
except BaseException as e:
@@ -148,7 +147,6 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No
meta = json.loads(gen.meta)
meta["error"] = (e.__class__.__name__ + ": " + str(e))
gen.meta = json.dumps(meta)
commit()
raise

View File

@@ -27,7 +27,7 @@ class Room(db.Entity):
seed = Required('Seed', index=True)
multisave = Optional(buffer, lazy=True)
show_spoiler = Required(int, default=0) # 0 -> never, 1 -> after completion, -> 2 always
timeout = Required(int, default=lambda: 6 * 60 * 60) # seconds since last activity to shutdown
timeout = Required(int, default=lambda: 2 * 60 * 60) # seconds since last activity to shutdown
tracker = Optional(UUID, index=True)
last_port = Optional(int, default=lambda: 0)

View File

@@ -60,7 +60,7 @@ def create():
for game_name, world in AutoWorldRegister.world_types.items():
all_options = {**world.options, **Options.per_game_common_options}
all_options = {**Options.per_game_common_options, **world.options}
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
options=all_options,
__version__=__version__, game=game_name, yaml_dump=yaml.dump,

View File

@@ -1,7 +1,7 @@
flask>=2.1.2
flask>=2.1.3
pony>=0.7.16
waitress>=2.1.1
flask-caching>=1.11.1
Flask-Caching>=2.0.1
Flask-Compress>=1.12
Flask-Limiter>=2.4.6
bokeh>=2.4.3
Flask-Limiter>=2.5.0
bokeh>=2.4.3

View File

@@ -49,6 +49,12 @@ If you are ready to start randomizing games, or want to start playing your favor
our discord server at the [Archipelago Discord](https://discord.gg/archipelago). There are always people ready to answer
any questions you might have.
## What are some common terms I should know?
As randomizers and multiworld randomizers have been around for a while now there are quite a lot of common terms
and jargon that is used in conjunction by the communities surrounding them. For a lot of the terms that are more common
to Archipelago and its specific systems please see the [Glossary](/glossary/en).
## I want to add a game to the Archipelago randomizer. How do I do that?
The best way to get started is to take a look at our code on GitHub

View File

@@ -0,0 +1,94 @@
# Multiworld Glossary
There are a lot of common terms used when playing in different game randomizer communities and in multiworld as well.
This document serves as a lookup for common terms that may be used by users in the community or in various other
documentation.
## Item
Items are what get shuffled around in your world or other worlds that you then receive. This could be a sword, a stat
upgrade, a spell, or any other potential receivable for your game.
## Location
Locations are where items are placed in your game. Whenever you interact with a location, you or another player will
then receive an item. A location could be a chest, an enemy drop, a shop purchase, or any other interactable that can
contain items in your game.
## Check
A check is a common term for when you "check", or pick up, a location. In terms of Archipelago this is usually used for
when a player goes to a location and sends its item, or "checks" the location. Players will often reference their now
randomized locations as checks.
## Slot
A slot is the player name and number assigned during generation. The number of slots is equal to the number of players,
or "worlds", created. Each name must be unique as these are used to identify the slot user.
## World
World in terms of Archipelago can mean multiple things and is used interchangeably in many situations.
* During gameplay, a world is a single instance of a game, occupying one player "slot". However,
Archipelago allows multiple players to connect to the same slot; then those players can share a world
and complete it cooperatively. For games with native cooperative play, you can also play together and
share a world that way, usually with only one player connected to the multiworld.
* On the programming side, a world typically represents the package that integrates Archipelago with a
particular game. For example this could be the entire `worlds/factorio` directory.
## RNG
Acronym for "Random Number Generator." Archipelago uses its own custom Random object with a unique seed per generation,
or, if running from source, a seed can be supplied and this seed will control all randomization during generation as all
game worlds will have access to it.
## Seed
A "seed" is a number used to initialize a pseudorandom number generator. Whenever you generate a new game on Archipelago
this is a new "seed" as it has unique item placement, and you can create multiple "rooms" on the Archipelago site from a
single seed. Using the same seed results in the random placement being the same.
## Room
Whenever you generate a seed on the Archipelago website you will be put on a seed page that contains all the seed info
with a link to the spoiler if one exists and will show how many unique rooms exist per seed. Each room has its own
unique identifier that is separate from the seed. The room page is where you can find information to connect to the
multiworld and download any patches if necessary. If you have a particularly fun or interesting seed, and you want to
share it with somebody you can link them to this seed page, where they can generate a new room to play it! For seeds
generated with race mode enabled, the seed page will only show rooms created by the unique user so the seed page is
perfectly safe to share for racing purposes.
## Logic
Base behavior of all seeds generated by Archipelago is they are expected to be completable based on the requirements of
the settings. This is done by using "logic" in order to determine valid locations to place items while still being able
to reach said location without this item. For the purposes of the randomizer a location is considered "in logic" if you
can reach it with your current toolset of items or skills based on settings. Some players are able to obtain locations
"out of logic" by performing various glitches or tricks that the settings may not account for and tend to mention this
when sending out an item they obtained this way.
## Progression
Certain items will allow access to more locations and are considered progression items as they "progress" the seed.
## Trash
A term used for "filler" items that have no bearing on the generation and are either marginally useful for the player
or useless. These items can be very useful depending on the player but are never very important and as such are usually
termed trash.
## Burger King / BK Mode
A term used in multiworlds when a player is unable to continue to progress and is awaiting an item. The term came to be
after a player, allegedly, was unable to progress during a multiworld and went to Burger King while waiting to receive
items from other players.
* "Logical BK" is when the player is unable to progress according to the settings of their game but may still be able to do
things that would be "out of logic" by the generation.
* "Hard / full BK" is when the player is completely unable to progress even with tricks they may know and are unable to
continue to play, aside from doing something like killing enemies for experience or money.
## Sphere
Archipelago calculates the game playthrough by using a "sphere" system where it has a state for each player and checks
to see what the players are able to reach with their current items. Any location that is reachable with the current
state of items is a "sphere." For the purposes of Archipelago it starts playthrough calculation by distributing sphere 0
items which are items that are either forced in the player's inventory by the game or placed in the `start_inventory` in
their settings. Sphere 1 is then all accessible locations the players can reach with all the items they received from
sphere 0, or their starting inventory. The playthrough continues in this fashion calculating a number of spheres until
all players have completed their goal.
## Scouts / Scouting
In some games there are locations that have visible items even if the item itself is unobtainable at the current time.
Some games utilize a scouting feature where when the player "sees" the item it will give a free hint for the item in the
client letting the players know what the exact item is, since if the item was for that game it would know but the item
being foreign is a lot harder to represent visually.

View File

@@ -0,0 +1,53 @@
window.addEventListener('load', () => {
const tutorialWrapper = document.getElementById('glossary-wrapper');
new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status === 404) {
reject("Sorry, the glossary page is not available in that language yet.");
return;
}
if (ajax.status !== 200) {
reject("Something went wrong while loading the glossary.");
return;
}
resolve(ajax.responseText);
};
ajax.open('GET', `${window.location.origin}/static/assets/faq/` +
`glossary_${tutorialWrapper.getAttribute('data-lang')}.md`, true);
ajax.send();
}).then((results) => {
// Populate page with HTML generated from markdown
showdown.setOption('tables', true);
showdown.setOption('strikethrough', true);
showdown.setOption('literalMidWordUnderscores', true);
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
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}`);
}
// 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);
}
}
}).catch((error) => {
console.error(error);
tutorialWrapper.innerHTML =
`<h2>This page is out of logic!</h2>
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
});
});

View File

@@ -23,6 +23,7 @@ window.addEventListener('load', () => {
games.forEach((game) => {
const gameTitle = document.createElement('h2');
gameTitle.innerText = game.gameTitle;
gameTitle.id = `${encodeURIComponent(game.gameTitle)}`;
tutorialDiv.appendChild(gameTitle);
game.tutorials.forEach((tutorial) => {
@@ -65,6 +66,15 @@ window.addEventListener('load', () => {
showError();
console.error(error);
}
// Check if we are on an anchor when coming in, and scroll to it.
const hash = window.location.hash;
if (hash) {
const offset = 128; // To account for navbar banner at top of page.
window.scrollTo(0, 0);
const rect = document.getElementById(hash.slice(1)).getBoundingClientRect();
window.scrollTo(rect.left, rect.top - offset);
}
};
ajax.open('GET', `${window.location.origin}/static/generated/tutorials.json`, true);
ajax.send();

View File

@@ -1,23 +1,27 @@
from collections import Counter, defaultdict
from itertools import cycle
from colorsys import hsv_to_rgb
from datetime import datetime, timedelta, date
from math import tau
import typing
from bokeh.embed import components
from bokeh.palettes import Dark2_8 as palette
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
from . import app, cache
from .models import Room
PLOT_WIDTH = 600
def get_db_data():
def get_db_data() -> typing.Tuple[typing.Dict[str, int], typing.Dict[datetime.date, typing.Dict[str, int]]]:
games_played = defaultdict(Counter)
total_games = Counter()
cutoff = date.today()-timedelta(days=30000)
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:
@@ -26,29 +30,72 @@ def get_db_data():
return total_games, games_played
def get_color_palette(colors_needed: int) -> typing.List[RGB]:
colors = []
# colors_needed +1 to prevent first and last color being too close to each other
colors_needed += 1
for x in range(0, 361, 360 // colors_needed):
# a bit of noise on value to add some luminosity difference
colors.append(RGB(*(val * 255 for val in hsv_to_rgb(x / 360, 0.8, 0.8 + (x / 1800)))))
# splice colors for maximum hue contrast.
colors = colors[::2] + colors[1::2]
return colors
def create_game_played_figure(all_games_data: typing.Dict[datetime.date, typing.Dict[str, int]],
game: str, color: RGB) -> figure:
occurences = []
days = [day for day, game_data in all_games_data.items() if game_data[game]]
for day in days:
occurences.append(all_games_data[day][game])
data = {
"days": [datetime.combine(day, datetime.min.time()) for day in days],
"played": occurences
}
plot = figure(
title=f"{game} Played Per Day", x_axis_type='datetime', x_axis_label="Date",
y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500,
toolbar_location=None, tools="",
# setting legend to False seems broken in bokeh currently?
# legend=False
)
hover = HoverTool(tooltips=[("Date:", "@days{%F}"), ("Played:", "@played")], formatters={"@days": "datetime"})
plot.add_tools(hover)
plot.vbar(x="days", top="played", legend_label=game, color=color, source=ColumnDataSource(data=data), width=1)
return plot
@app.route('/stats')
@cache.memoize(timeout=60*60) # regen once per hour should be plenty
@cache.memoize(timeout=60 * 60) # regen once per hour should be plenty
def stats():
plot = figure(title="Games Played Per Day", x_axis_type='datetime', x_axis_label="Date",
y_axis_label="Games Played", sizing_mode="scale_both", width=500, height=500)
y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500)
total_games, games_played = get_db_data()
days = sorted(games_played)
cyc_palette = cycle(palette)
color_palette = get_color_palette(len(total_games))
game_to_color: typing.Dict[str, RGB] = {game: color for game, color in zip(total_games, color_palette)}
for game in sorted(total_games):
occurences = []
for day in days:
occurences.append(games_played[day][game])
plot.line([datetime.combine(day, datetime.min.time()) for day in days],
occurences, legend_label=game, line_width=2, color=next(cyc_palette))
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,
tools="hover", tooltips=[("Game:", "@games"), ("Played:", "@count")],
sizing_mode="scale_both", width=500, height=500)
sizing_mode="scale_both", width=PLOT_WIDTH, height=500, x_range=(-0.5, 1.2))
pie.axis.visible = False
pie.xgrid.visible = False
pie.ygrid.visible = False
data = {
"games": [],
@@ -65,12 +112,15 @@ def stats():
current_angle += angle
data["end_angles"].append(current_angle)
data["colors"] = [element[1] for element in sorted((game, color) for game, color in
zip(data["games"], cycle(palette)))]
pie.wedge(x=0.5, y=0.5, radius=0.5,
data["colors"] = [game_to_color[game] for game in data["games"]]
pie.wedge(x=0, y=0, radius=0.5,
start_angle="start_angles", end_angle="end_angles", fill_color="colors",
source=ColumnDataSource(data=data), legend_field="games")
script, charts = components((plot, pie))
per_game_charts = [create_game_played_figure(games_played, game, game_to_color[game]) for game in total_games
if total_games[game] > 1]
script, charts = components((plot, pie, *per_game_charts))
return render_template("stats.html", js_resources=INLINE.render_js(), css_resources=INLINE.render_css(),
chart_data=script, charts=charts)

View File

@@ -0,0 +1,17 @@
{% extends 'pageWrapper.html' %}
{% block head %}
{% include 'header/grassHeader.html' %}
<title>Glossary</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
crossorigin="anonymous"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/glossary.js") }}"></script>
{% endblock %}
{% block body %}
<div id="glossary-wrapper" data-lang="{{ lang }}" class="markdown">
<!-- Content generated by JavaScript -->
</div>
{% endblock %}

View File

@@ -16,9 +16,9 @@
This room has a <a href="{{ url_for("getTracker", tracker=room.tracker) }}">Multiworld Tracker</a> enabled.
<br />
{% endif %}
This room will be closed after {{ room.timeout//60//60 }} hours of inactivity. Should you wish to continue
later,
you can simply refresh this page and the server will be started again.<br>
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
Should you wish to continue later,
anyone can simply refresh this page and the server will resume.<br>
{% if room.last_port %}
You can connect to this room by using <span class="interactive"
data-tooltip="This means address/ip is {{ config['PATCH_TARGET'] }} and port is {{ room.last_port }}.">

View File

@@ -40,9 +40,12 @@
{% elif patch.game == "Super Mario 64" and room.seed.slots|length == 1 %}
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
Download APSM64EX File...</a>
{% elif patch.game in ["A Link to the Past", "Secret of Evermore", "Super Metroid", "SMZ3"] %}
{% elif patch.game | supports_apdeltapatch %}
<a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}" download>
Download Patch File...</a>
{% elif patch.game == "Dark Souls III" %}
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
Download JSON File...</a>
{% else %}
No file to download for this game.
{% endif %}

View File

@@ -26,6 +26,7 @@
<li><a href="/user-content">User Content</a></li>
<li><a href="/weighted-settings">Weighted Settings Page</a></li>
<li><a href="{{url_for('stats')}}">Game Statistics</a></li>
<li><a href="/glossary/en">Glossary</a></li>
</ul>
<h2>Game Info Pages</h2>

View File

@@ -15,10 +15,15 @@
<p>
{{ world.__doc__ | default("No description provided.", true) }}<br />
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
{% if world.web.tutorials %}
<span class="link-spacer">|</span>
<a href="{{ url_for("tutorial_landing") }}#{{ game_name }}">Setup Guides</a>
{% endif %}
{% if world.web.settings_page is string %}
<span class="link-spacer">|</span>
<a href="{{ world.web.settings_page }}">Settings Page</a>
{% elif world.web.settings_page %}
<span class="link-spacer">|</span>
<a href="{{ url_for("player_settings", game=game_name) }}">Settings Page</a>
{% endif %}
{% if world.web.bug_report_page %}

View File

@@ -80,6 +80,11 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
player_id=int(slot_id[1:]), game="Ocarina of Time"))
elif file.filename.endswith(".json"):
_, seed_name, slot_id, slot_name = file.filename.split('.')[0].split('-', 3)
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
player_id=int(slot_id[1:]), game="Dark Souls III"))
elif file.filename.endswith(".txt"):
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")

View File

@@ -2,8 +2,8 @@ local socket = require("socket")
local json = require('json')
local math = require('math')
local last_modified_date = '2022-05-25' -- Should be the last modified date
local script_version = 1
local last_modified_date = '2022-07-24' -- Should be the last modified date
local script_version = 2
--------------------------------------------------
-- Heavily modified form of RiptideSage's tracker
@@ -1723,6 +1723,11 @@ function get_death_state()
end
function kill_link()
-- market entrance: 27/28/29
-- outside ToT: 35/36/37.
-- if killed on these scenes the game crashes, so we wait until not on this screen.
local scene = global_context:rawget('cur_scene'):rawget()
if scene == 27 or scene == 28 or scene == 29 or scene == 35 or scene == 36 or scene == 37 then return end
mainmemory.write_u16_be(0x11A600, 0)
end
@@ -1824,13 +1829,15 @@ function main()
elseif (curstate == STATE_UNINITIALIZED) then
if (frame % 60 == 0) then
server:settimeout(2)
print("Attempting to connect")
local client, timeout = server:accept()
if timeout == nil then
print('Initial Connection Made')
curstate = STATE_INITIAL_CONNECTION_MADE
ootSocket = client
ootSocket:settimeout(0)
else
print('Connection failed, ensure OoTClient is running and rerun oot_connector.lua')
return
end
end
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 374 KiB

After

Width:  |  Height:  |  Size: 246 KiB

View File

@@ -8,6 +8,15 @@ flowchart LR
CC[CommonClient.py]
AS <-- WebSockets --> CC
subgraph "Starcraft 2"
SC2[Starcraft 2 Game Client]
SC2C[Starcraft2Client.py]
SC2AI[apsc2 Python Package]
SC2C <--> SC2AI <-- WebSockets --> SC2
end
CC <-- Integrated --> SC2C
%% ChecksFinder
subgraph ChecksFinder
CFC[ChecksFinderClient]
@@ -72,12 +81,14 @@ flowchart LR
V6[VVVVVV]
MT[Meritous]
TW[The Witness]
SA2B[Sonic Adventure 2: Battle]
APCLIENTPP <--> SOE
APCLIENTPP <--> MT
APCLIENTPP <-- The Witness Randomizer --> TW
APCPP <--> SM64
APCPP <--> V6
APCPP <--> SA2B
end
SOE <--> SNI <-- Various, depending on SNES device --> SOESNES
AS <-- WebSockets --> APCLIENTPP

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -191,8 +191,23 @@ Sent to clients after a client requested this message be sent to them, more info
### InvalidPacket
Sent to clients if the server caught a problem with a packet. This only occurs for errors that are explicitly checked for.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| type | str | The [PacketProblemType](#PacketProblemType) that was detected in the packet. |
| original_cmd | Optional[str] | The `cmd` argument of the faulty packet, will be `None` if the `cmd` failed to be parsed. |
| text | str | A descriptive message of the problem at hand. |
##### PacketProblemType
`PacketProblemType` indicates the type of problem that was detected in the faulty packet, the known problem types are below but others may be added in the future.
| Type | Notes |
| ---- | ----- |
| cmd | `cmd` argument of the faulty packet that could not be parsed correctly. |
| arguments | Arguments of the faulty packet which were not correct. |
### Retrieved
Sent to clients as a response the a [Get](#Get) package
Sent to clients as a response the a [Get](#Get) package.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |

View File

@@ -0,0 +1,58 @@
# Running From Source
If you just want to play and there is a compiled version available on the
[Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases),
use that version. These steps are for developers or platforms without compiled releases available.
## General
What you'll need:
* Python 3.8.7 or newer
* pip (Depending on platform may come included)
* A C compiler
* possibly optional, read OS-specific sections
Then run any of the starting point scripts, like Generate.py, and the included ModuleUpdater should prompt to install or update the
required modules and after pressing enter proceed to install everything automatically.
After this, you should be able to run the programs.
## Windows
Recommended steps
* Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads)
* Download and install full Visual Studio from
[Visual Studio Downloads](https://visualstudio.microsoft.com/downloads/)
or an older "Build Tools for Visual Studio" from
[Visual Studio Older Downloads](https://visualstudio.microsoft.com/vs/older-downloads/).
* Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details
* This step is optional. Pre-compiled modules are pinned on
[Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
* Run Generate.py which will prompt installation of missing modules, press enter to confirm
## macOS
Refer to [Guide to Run Archipelago from Source Code on macOS](../worlds/generic/docs/mac_en.md).
## Optional: A Link to the Past Enemizer
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
error if it is required.
You can get the latest Enemizer release at [Enemizer Github releases](https://github.com/Ijwu/Enemizer/releases).
It should be dropped as "EnemizerCLI" into the root folder of the project. Alternatively, you can point the Enemizer
setting in host.yaml at your Enemizer executable.
## Optional: SNI
SNI is required to use SNIClient. If not integrated into the project, it has to be started manually.
You can get the latest SNI release at [SNI Github releases](https://github.com/alttpo/sni/releases).
It should be dropped as "SNI" into the root folder of the project. Alternatively, you can point the sni setting in
host.yaml at your SNI folder.

49
docs/style.md Normal file
View File

@@ -0,0 +1,49 @@
# Style Guide
## Generic
* This guide can be ignored for data files that are not to be viewed in an editor.
* 120 character per line for all source files.
* Avoid white space errors like trailing spaces.
## Python Code
* We mostly follow [PEP8](https://peps.python.org/pep-0008/). Read below to see the differences.
* 120 characters per line. PyCharm does this automatically, other editors can be configured for it.
* Strings in core code will be `"strings"`. In other words: double quote your strings.
* Strings in worlds should use double quotes as well, but imported code may differ.
* Prefer [format string literals](https://peps.python.org/pep-0498/) over string concatenation,
use single quotes inside them: `f"Like {dct['key']}"`
* Use type annotation where possible.
## Markdown
* We almost follow [Google's styleguide](https://google.github.io/styleguide/docguide/style.html).
Read below for differences.
* For existing documents, try to follow its style or ask to completely reformat it.
* 120 characters per line.
* One space between bullet/number and text.
* No lazy numbering.
## HTML
* Indent with 2 spaces for new code.
* kebab-case for ids and classes.
## CSS
* Indent with 2 spaces for new code.
* `{` on the same line as the selector.
* No space between selector and `{`.
## JS
* Indent with 2 spaces.
* Indent `case` inside `switch ` with 2 spaces.
* Use single quotes.
* Semicolons are required after every statement.

View File

@@ -236,7 +236,7 @@ class MyGameLocation(Location):
game: str = "My Game"
# override constructor to automatically mark event locations as such
def __init__(self, player: int, name = '', code = None, parent = None):
def __init__(self, player: int, name = "", code = None, parent = None):
super(MyGameLocation, self).__init__(player, name, code, parent)
self.event = code is None
```
@@ -487,14 +487,14 @@ def create_items(self) -> None:
for item in map(self.create_item, mygame_items):
if item in exclude:
exclude.remove(item) # this is destructive. create unique list above
self.world.itempool.append(self.create_item('nothing'))
self.world.itempool.append(self.create_item("nothing"))
else:
self.world.itempool.append(item)
# itempool and number of locations should match up.
# If this is not the case we want to fill the itempool with junk.
junk = 0 # calculate this based on player settings
self.world.itempool += [self.create_item('nothing') for _ in range(junk)]
self.world.itempool += [self.create_item("nothing") for _ in range(junk)]
```
#### create_regions
@@ -628,7 +628,7 @@ class MyGameLogic(LogicMixin):
def _mygame_has_key(self, world: MultiWorld, player: int):
# Arguments above are free to choose
# it may make sense to use World as argument instead of MultiWorld
return self.has('key', player) # or whatever
return self.has("key", player) # or whatever
```
```python
# __init__.py

View File

@@ -101,7 +101,9 @@ sm_options:
# Alternatively, a path to a program to open the .sfc file with
rom_start: true
factorio_options:
executable: "factorio\\bin\\x64\\factorio"
executable: "factorio/bin/x64/factorio"
# by default, no settings are loaded if this file does not exist. If this file does exist, then it will be used.
# server_settings: "factorio\\data\\server-settings.json"
minecraft_options:
forge_directory: "Minecraft Forge server"
max_heap_size: "2G"
@@ -127,3 +129,12 @@ smz3_options:
# True for operating system default program
# Alternatively, a path to a program to open the .sfc file with
rom_start: true
dkc3_options:
# File name of the DKC3 US rom
rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
sni: "SNI"
# Set this to false to never autostart a rom (such as after patching)
# True for operating system default program
# Alternatively, a path to a program to open the .sfc file with
rom_start: true

View File

@@ -54,6 +54,7 @@ Name: "custom"; Description: "Custom installation"; Flags: iscustom
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
Name: "generator"; Description: "Generator"; Types: full hosting
Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
Name: "generator/dkc3"; Description: "Donkey Kong Country 3 ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning
@@ -62,6 +63,7 @@ Name: "client"; Description: "Clients"; Types: full playing
Name: "client/sni"; Description: "SNI Client"; Types: full playing
Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing
@@ -76,6 +78,7 @@ NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-mod
[Files]
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/sni/lttp or generator/lttp
Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).sfc"; Flags: external; Components: client/sni/sm or generator/sm
Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"; Flags: external; Components: client/sni/dkc3 or generator/dkc3
Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
@@ -129,6 +132,7 @@ Type: dirifempty; Name: "{app}"
[InstallDelete]
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy*"
[Registry]
@@ -142,6 +146,11 @@ Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archi
Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apdkc3"; ValueData: "{#MyAppName}dkc3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}dkc3patch"; ValueData: "Archipelago Donkey Kong Country 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}dkc3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}dkc3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apsmz3"; ValueData: "{#MyAppName}smz3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smz3patch"; ValueData: "Archipelago SMZ3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smz3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
@@ -205,6 +214,9 @@ var LttPROMFilePage: TInputFileWizardPage;
var smrom: string;
var SMRomFilePage: TInputFileWizardPage;
var dkc3rom: string;
var DKC3RomFilePage: TInputFileWizardPage;
var soerom: string;
var SoERomFilePage: TInputFileWizardPage;
@@ -294,6 +306,8 @@ begin
Result := not (LttPROMFilePage.Values[0] = '')
else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then
Result := not (SMROMFilePage.Values[0] = '')
else if (assigned(DKC3ROMFilePage)) and (CurPageID = DKC3ROMFilePage.ID) then
Result := not (DKC3ROMFilePage.Values[0] = '')
else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then
Result := not (SoEROMFilePage.Values[0] = '')
else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then
@@ -334,6 +348,22 @@ begin
Result := '';
end;
function GetDKC3ROMPath(Param: string): string;
begin
if Length(dkc3rom) > 0 then
Result := dkc3rom
else if Assigned(DKC3RomFilePage) then
begin
R := CompareStr(GetSNESMD5OfFile(DKC3ROMFilePage.Values[0]), '120abf304f0c40fe059f6a192ed4f947')
if R <> 0 then
MsgBox('Donkey Kong Country 3 ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := DKC3ROMFilePage.Values[0]
end
else
Result := '';
end;
function GetSoEROMPath(Param: string): string;
begin
if Length(soerom) > 0 then
@@ -378,6 +408,10 @@ begin
if Length(smrom) = 0 then
SMRomFilePage:= AddRomPage('Super Metroid (JU).sfc');
dkc3rom := CheckRom('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc', '120abf304f0c40fe059f6a192ed4f947');
if Length(dkc3rom) = 0 then
DKC3RomFilePage:= AddRomPage('Donkey Kong Country 3 - Dixie Kong''s Double Trouble! (USA) (En,Fr).sfc');
soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a');
if Length(soerom) = 0 then
SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc');
@@ -391,6 +425,8 @@ begin
Result := not (WizardIsComponentSelected('client/sni/lttp') or WizardIsComponentSelected('generator/lttp'));
if (assigned(SMROMFilePage)) and (PageID = SMROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/sni/sm') or WizardIsComponentSelected('generator/sm'));
if (assigned(DKC3ROMFilePage)) and (PageID = DKC3ROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/sni/dkc3') or WizardIsComponentSelected('generator/dkc3'));
if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/soe'));
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then

11
kvui.py
View File

@@ -334,8 +334,8 @@ class GameManager(App):
# top part
server_label = ServerLabel()
self.connect_layout.add_widget(server_label)
self.server_connect_bar = ConnectBarTextInput(text="archipelago.gg", size_hint_y=None, height=30, multiline=False,
write_tab=False)
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.server_address or "archipelago.gg", size_hint_y=None,
height=30, multiline=False, write_tab=False)
self.server_connect_bar.bind(on_text_validate=self.connect_button_action)
self.connect_layout.add_widget(self.server_connect_bar)
self.server_connect_button = Button(text="Connect", size=(100, 30), size_hint_y=None, size_hint_x=None)
@@ -412,6 +412,7 @@ class GameManager(App):
def connect_button_action(self, button):
if self.ctx.server:
self.ctx.server_address = None
self.ctx.username = None
asyncio.create_task(self.ctx.disconnect())
else:
asyncio.create_task(self.ctx.connect(self.server_connect_bar.text.replace("/connect ", "")))
@@ -445,6 +446,12 @@ class GameManager(App):
self.log_panels["Archipelago"].on_message_markup(text)
self.log_panels["All"].on_message_markup(text)
def update_address_bar(self, text: str):
if hasattr(self, "server_connect_bar"):
self.server_connect_bar.text = text
else:
logging.getLogger("Client").info("Could not update address bar as the GUI is not yet initialized.")
def enable_energy_link(self):
if not hasattr(self, "energy_link_label"):
self.energy_link_label = Label(text="Energy Link: Standby",

View File

@@ -175,12 +175,15 @@ A Link to the Past:
retro_caves:
on: 0 # Zelda-1 like mode. There are randomly placed take-any caves that contain one Sword and choices of Heart Container/Blue Potion.
off: 50
hints: # Vendors: King Zora and Bottle Merchant say what they're selling.
# On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints.
hints: # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints.
'on': 50
vendors: 0
'off': 0
full: 0
scams: # If on, these Merchants will no longer tell you what they're selling.
'off': 50
'king_zora': 0
'bottle_merchant': 0
'all': 0
swordless:
on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
off: 1
@@ -273,6 +276,7 @@ A Link to the Past:
p: 0 # Randomize the prices of the items in shop inventories
u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too
P: 0 # Prices of the items in shop inventories cost hearts, arrow, or bombs instead of rupees
ip: 0 # Shuffle inventories and randomize prices
fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool
uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool

View File

@@ -1,4 +1,4 @@
colorama>=0.4.4
colorama>=0.4.5
websockets>=10.3
PyYAML>=6.0
jellyfish>=0.9.0

View File

@@ -1,5 +1,6 @@
import typing
from BaseClasses import MultiWorld
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink
@@ -27,6 +28,35 @@ class Goal(Choice):
option_hand_in = 2
class OpenPyramid(Choice):
"""Determines whether the hole at the top of pyramid is open.
Goal will open the pyramid if the goal requires you to kill Ganon, without needing to kill Agahnim 2.
Auto is the same as goal except if Ganon's dropdown is in another location, the hole will be closed."""
display_name = "Open Pyramid Hole"
option_closed = 0
option_open = 1
option_goal = 2
option_auto = 3
default = option_goal
alias_true = option_open
alias_false = option_closed
alias_yes = option_open
alias_no = option_closed
def to_bool(self, world: MultiWorld, player: int) -> bool:
if self.value == self.option_goal:
return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
elif self.value == self.option_auto:
return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} \
and (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not
world.shuffle_ganon)
elif self.value == self.option_open:
return True
else:
return False
class DungeonItem(Choice):
value: int
option_original_dungeon = 0
@@ -185,9 +215,11 @@ class Scams(Choice):
option_all = 3
alias_false = 0
@property
def gives_king_zora_hint(self):
return self.value in {0, 2}
@property
def gives_bottle_merchant_hint(self):
return self.value in {0, 1}
@@ -331,6 +363,7 @@ class AllowCollect(Toggle):
alttp_options: typing.Dict[str, type(Option)] = {
"crystals_needed_for_gt": CrystalsTower,
"crystals_needed_for_ganon": CrystalsGanon,
"open_pyramid": OpenPyramid,
"bigkey_shuffle": bigkey_shuffle,
"smallkey_shuffle": smallkey_shuffle,
"compass_shuffle": compass_shuffle,

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import Utils
from Patch import read_rom
JAP10HASH = '03a63945398191337e896e5771f77173'
LTTPJPN10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '9952c2a3ec1b421e408df0d20c8f0c7f'
ROM_PLAYER_LIMIT = 255
@@ -19,7 +19,7 @@ import threading
import xxtea
import concurrent.futures
import bsdiff4
from typing import Optional
from typing import Optional, List
from BaseClasses import CollectionState, Region, Location
from worlds.alttp.Shops import ShopType, ShopPriceType
@@ -186,7 +186,7 @@ def check_enemizer(enemizercli):
# some time may have passed since the lock was acquired, as such a quick re-check doesn't hurt
if getattr(check_enemizer, "done", None):
return
wanted_version = (7, 0, 1)
# version info is saved on the lib, for some reason
library_info = os.path.join(os.path.dirname(enemizercli), "EnemizerCLI.Core.deps.json")
with open(library_info) as f:
@@ -197,10 +197,11 @@ def check_enemizer(enemizercli):
version = lib.split("/")[-1]
version = tuple(int(element) for element in version.split("."))
enemizer_logger.debug(f"Found Enemizer version {version}")
if version < (6, 4, 0):
if version < wanted_version:
raise Exception(
f"Enemizer found at {enemizercli} is outdated ({info}), please update your Enemizer. "
f"Such as https://github.com/Ijwu/Enemizer/releases")
f"Enemizer found at {enemizercli} is outdated ({version}) < ({wanted_version}), "
f"please update your Enemizer. "
f"Such as from https://github.com/Ijwu/Enemizer/releases")
break
else:
raise Exception(f"Could not find Enemizer library version information in {library_info}")
@@ -1246,7 +1247,7 @@ def patch_rom(world, rom, player, enemized):
rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest
rom.write_byte(0x50599, 0x00) # disable below ganon chest
rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest
rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player] else 0x00) # pre-open Pyramid Hole
rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player].to_bool(world, player) else 0x00) # pre-open Pyramid Hole
rom.write_byte(0x18008C, 0x01 if world.crystals_needed_for_gt[
player] == 0 else 0x00) # GT pre-opened if crystal requirement is 0
rom.write_byte(0xF5D73, 0xF0) # bees are catchable
@@ -1645,8 +1646,7 @@ def patch_rom(world, rom, player, enemized):
# set rom name
# 21 bytes
from Main import __version__
# TODO: Adjust Enemizer to accept AP and AD
rom.name = bytearray(f'BM{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
rom.name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x7FC0, rom.name)
@@ -1677,7 +1677,7 @@ def patch_race_rom(rom, world, player):
rom.encrypt(world, player)
def get_price_data(price: int, price_type: int) -> bytes:
def get_price_data(price: int, price_type: int) -> List[int]:
if price_type != ShopPriceType.Rupees:
# Set special price flag 0x8000
# Then set the type of price we're setting 0x7F00 (this starts from Hearts, not Rupees, subtract 1)
@@ -2890,7 +2890,7 @@ hash_alphabet = [
class LttPDeltaPatch(Patch.APDeltaPatch):
hash = JAP10HASH
hash = LTTPJPN10HASH
game = "A Link to the Past"
patch_file_ending = ".aplttp"
@@ -2907,8 +2907,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if JAP10HASH != basemd5.hexdigest():
raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. '
if LTTPJPN10HASH != basemd5.hexdigest():
raise Exception('Supplied Base Rom does not match known MD5 for Japan(1.0) release. '
'Get the correct game and version, then dump it')
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
return base_rom_bytes

View File

@@ -460,10 +460,11 @@ def shuffle_shops(world, items, player: int):
f"Not all upgrades put into Player{player}' item pool. Putting remaining items in Capacity Upgrade shop instead.")
bombupgrades = sum(1 for item in new_items if 'Bomb Upgrade' in item)
arrowupgrades = sum(1 for item in new_items if 'Arrow Upgrade' in item)
slots = iter(range(2))
if bombupgrades:
capacityshop.add_inventory(1, 'Bomb Upgrade (+5)', 100, bombupgrades)
capacityshop.add_inventory(next(slots), 'Bomb Upgrade (+5)', 100, bombupgrades)
if arrowupgrades:
capacityshop.add_inventory(1, 'Arrow Upgrade (+5)', 100, arrowupgrades)
capacityshop.add_inventory(next(slots), 'Arrow Upgrade (+5)', 100, arrowupgrades)
else:
for item in new_items:
world.push_precollected(ItemFactory(item, player))

View File

@@ -176,17 +176,6 @@ class ALTTPWorld(World):
def create_regions(self):
player = self.player
world = self.world
if world.open_pyramid[player] == 'goal':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt',
'localganontriforcehunt', 'ganonpedestal'}
elif world.open_pyramid[player] == 'auto':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt',
'localganontriforcehunt', 'ganonpedestal'} and \
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull',
'dungeonscrossed'} or not world.shuffle_ganon)
else:
world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(
world.open_pyramid[player], 'auto')
world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player],
world.triforce_pieces_required[player])

View File

@@ -144,7 +144,8 @@ Sólo hay que segiur estos pasos una vez.
2. Ve a Ajustes --> Interfaz de usario. Configura "Mostrar ajustes avanzados" en ON.
3. Ve a Ajustes --> Red. Configura "Comandos de red" en ON. (Se encuentra bajo Request Device 16.) Deja en 55355 (el
default) el Puerto de comandos de red.
![Captura de pantalla del ajuste Comandos de red](/static/assets/tutorial/retroarch-network-commands-en.png)
![Captura de pantalla del ajuste Comandos de red](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png)
4. Ve a Menú principal --> Actualizador en línea --> Descargador de núcleos. Desplázate y selecciona "Nintendo - SNES /
SFC (bsnes-mercury Performance)".

View File

@@ -299,4 +299,5 @@ item_table = (
'A Shrubbery',
'Roomba with a Knife',
'Wet Cat',
'The missing moderator, Frostwares',
)

View File

@@ -25,7 +25,7 @@ class ArchipIDLEWorld(World):
"""
game = "ArchipIDLE"
topology_present = False
data_version = 3
data_version = 4
hidden = (datetime.now().month != 4) # ArchipIDLE is only visible during April
web = ArchipIDLEWebWorld()

View File

@@ -0,0 +1,41 @@
import typing
from Options import Toggle, Option
class AutoEquipOption(Toggle):
"""Automatically equips any received armor or left/right weapons."""
display_name = "Auto-equip"
class LockEquipOption(Toggle):
"""Lock the equipment slots so you cannot change your armor or your left/right weapons. Works great with the
Auto-equip option."""
display_name = "Lock Equipement Slots"
class NoWeaponRequirementsOption(Toggle):
"""Disable the weapon requirements by removing any movement or damage penalties.
Permitting you to use any weapon early"""
display_name = "No Weapon Requirements"
class RandomizeWeaponsLevelOption(Toggle):
"""Enable this option to upgrade 33% ( based on the probability chance ) of the pool of weapons to a random value
between +1 and +5/+10"""
display_name = "Randomize weapons level"
class LateBasinOfVowsOption(Toggle):
"""Force the Basin of Vows to be located as a reward of defeating Pontiff Sulyvahn. It permits to ease the
progression by preventing having to kill the Dancer of the Boreal Valley as the first boss"""
display_name = "Late Basin of Vows"
dark_souls_options: typing.Dict[str, type(Option)] = {
"auto_equip": AutoEquipOption,
"lock_equip": LockEquipOption,
"no_weapon_requirements": NoWeaponRequirementsOption,
"randomize_weapons_level": RandomizeWeaponsLevelOption,
"late_basin_of_vows": LateBasinOfVowsOption,
}

View File

@@ -0,0 +1,287 @@
# world/dark_souls_3/__init__.py
import json
import os
from .Options import dark_souls_options
from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary_table, key_items_list
from .data.locations_data import location_dictionary_table, cemetery_of_ash_table, fire_link_shrine_table, \
high_wall_of_lothric, \
undead_settlement_table, road_of_sacrifice_table, consumed_king_garden_table, cathedral_of_the_deep_table, \
farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table, \
irithyll_dungeon_table, profaned_capital_table, anor_londo_table, lothric_castle_table, grand_archives_table, \
untended_graves_table, archdragon_peak_table, firelink_shrine_bell_tower_table
from ..AutoWorld import World, WebWorld
from BaseClasses import MultiWorld, Location, Region, Item, RegionType, Entrance, Tutorial, ItemClassification
from ..generic.Rules import set_rule
class DarkSouls3Web(WebWorld):
tutorials = [Tutorial(
"Multiworld Setup Tutorial",
"A guide to setting up the Archipelago Dark Souls III randomizer on your computer.",
"English",
"setup_en.md",
"setup/en",
["Marech"]
)]
class DarkSouls3World(World):
"""
Dark souls III is an Action role-playing game and is part of the Souls series developed by FromSoftware.
Played in a third-person perspective, players have access to various weapons, armour, magic, and consumables that
they can use to fight their enemies.
"""
game: str = "Dark Souls III"
options = dark_souls_options
topology_present: bool = True
remote_items: bool = False
remote_start_inventory: bool = False
web = DarkSouls3Web()
data_version = 2
base_id = 100000
item_name_to_id = {name: id for id, name in enumerate(item_dictionary_table, base_id)}
location_name_to_id = {name: id for id, name in enumerate(location_dictionary_table, base_id)}
def __init__(self, world: MultiWorld, player: int):
super().__init__(world, player)
self.locked_items = []
self.locked_locations = []
self.main_path_locations = []
def create_item(self, name: str) -> Item:
data = self.item_name_to_id[name]
if name in key_items_list:
item_classification = ItemClassification.progression
elif name in weapons_upgrade_5_table or name in weapons_upgrade_10_table:
item_classification = ItemClassification.useful
else:
item_classification = ItemClassification.filler
return DarkSouls3Item(name, item_classification, data, self.player)
def create_regions(self):
menu_region = Region("Menu", RegionType.Generic, "Menu", self.player)
self.world.regions.append(menu_region)
# Create all Vanilla regions of Dark Souls III
cemetery_of_ash_region = self.create_region("Cemetery Of Ash", cemetery_of_ash_table)
firelink_shrine_region = self.create_region("Firelink Shrine", fire_link_shrine_table)
firelink_shrine_bell_tower_region = self.create_region("Firelink Shrine Bell Tower",
firelink_shrine_bell_tower_table)
high_wall_of_lothric_region = self.create_region("High Wall of Lothric", high_wall_of_lothric)
undead_settlement_region = self.create_region("Undead Settlement", undead_settlement_table)
road_of_sacrifices_region = self.create_region("Road of Sacrifices", road_of_sacrifice_table)
consumed_king_garden_region = self.create_region("Consumed King's Garden", consumed_king_garden_table)
cathedral_of_the_deep_region = self.create_region("Cathedral of the Deep", cathedral_of_the_deep_table)
farron_keep_region = self.create_region("Farron Keep", farron_keep_table)
catacombs_of_carthus_region = self.create_region("Catacombs of Carthus", catacombs_of_carthus_table)
smouldering_lake_region = self.create_region("Smouldering Lake", smouldering_lake_table)
irithyll_of_the_boreal_valley_region = self.create_region("Irithyll of the Boreal Valley",
irithyll_of_the_boreal_valley_table)
irithyll_dungeon_region = self.create_region("Irithyll Dungeon", irithyll_dungeon_table)
profaned_capital_region = self.create_region("Profaned Capital", profaned_capital_table)
anor_londo_region = self.create_region("Anor Londo", anor_londo_table)
lothric_castle_region = self.create_region("Lothric Castle", lothric_castle_table)
grand_archives_region = self.create_region("Grand Archives", grand_archives_table)
untended_graves_region = self.create_region("Untended Graves", untended_graves_table)
archdragon_peak_region = self.create_region("Archdragon Peak", archdragon_peak_table)
kiln_of_the_first_flame_region = self.create_region("Kiln Of The First Flame", None)
# Create the entrance to connect those regions
menu_region.exits.append(Entrance(self.player, "New Game", menu_region))
self.world.get_entrance("New Game", self.player).connect(cemetery_of_ash_region)
cemetery_of_ash_region.exits.append(Entrance(self.player, "Goto Firelink Shrine", cemetery_of_ash_region))
self.world.get_entrance("Goto Firelink Shrine", self.player).connect(firelink_shrine_region)
firelink_shrine_region.exits.append(Entrance(self.player, "Goto High Wall of Lothric",
firelink_shrine_region))
firelink_shrine_region.exits.append(Entrance(self.player, "Goto Kiln Of The First Flame",
firelink_shrine_region))
firelink_shrine_region.exits.append(Entrance(self.player, "Goto Bell Tower",
firelink_shrine_region))
self.world.get_entrance("Goto High Wall of Lothric", self.player).connect(high_wall_of_lothric_region)
self.world.get_entrance("Goto Kiln Of The First Flame", self.player).connect(kiln_of_the_first_flame_region)
self.world.get_entrance("Goto Bell Tower", self.player).connect(firelink_shrine_bell_tower_region)
high_wall_of_lothric_region.exits.append(Entrance(self.player, "Goto Undead Settlement",
high_wall_of_lothric_region))
high_wall_of_lothric_region.exits.append(Entrance(self.player, "Goto Lothric Castle",
high_wall_of_lothric_region))
self.world.get_entrance("Goto Undead Settlement", self.player).connect(undead_settlement_region)
self.world.get_entrance("Goto Lothric Castle", self.player).connect(lothric_castle_region)
undead_settlement_region.exits.append(Entrance(self.player, "Goto Road Of Sacrifices",
undead_settlement_region))
self.world.get_entrance("Goto Road Of Sacrifices", self.player).connect(road_of_sacrifices_region)
road_of_sacrifices_region.exits.append(Entrance(self.player, "Goto Cathedral", road_of_sacrifices_region))
road_of_sacrifices_region.exits.append(Entrance(self.player, "Goto Farron keep", road_of_sacrifices_region))
self.world.get_entrance("Goto Cathedral", self.player).connect(cathedral_of_the_deep_region)
self.world.get_entrance("Goto Farron keep", self.player).connect(farron_keep_region)
farron_keep_region.exits.append(Entrance(self.player, "Goto Carthus catacombs", farron_keep_region))
self.world.get_entrance("Goto Carthus catacombs", self.player).connect(catacombs_of_carthus_region)
catacombs_of_carthus_region.exits.append(Entrance(self.player, "Goto Irithyll of the boreal",
catacombs_of_carthus_region))
catacombs_of_carthus_region.exits.append(Entrance(self.player, "Goto Smouldering Lake",
catacombs_of_carthus_region))
self.world.get_entrance("Goto Irithyll of the boreal", self.player).\
connect(irithyll_of_the_boreal_valley_region)
self.world.get_entrance("Goto Smouldering Lake", self.player).connect(smouldering_lake_region)
irithyll_of_the_boreal_valley_region.exits.append(Entrance(self.player, "Goto Irithyll dungeon",
irithyll_of_the_boreal_valley_region))
irithyll_of_the_boreal_valley_region.exits.append(Entrance(self.player, "Goto Anor Londo",
irithyll_of_the_boreal_valley_region))
self.world.get_entrance("Goto Irithyll dungeon", self.player).connect(irithyll_dungeon_region)
self.world.get_entrance("Goto Anor Londo", self.player).connect(anor_londo_region)
irithyll_dungeon_region.exits.append(Entrance(self.player, "Goto Archdragon peak", irithyll_dungeon_region))
irithyll_dungeon_region.exits.append(Entrance(self.player, "Goto Profaned capital", irithyll_dungeon_region))
self.world.get_entrance("Goto Archdragon peak", self.player).connect(archdragon_peak_region)
self.world.get_entrance("Goto Profaned capital", self.player).connect(profaned_capital_region)
lothric_castle_region.exits.append(Entrance(self.player, "Goto Consumed King Garden", lothric_castle_region))
lothric_castle_region.exits.append(Entrance(self.player, "Goto Grand Archives", lothric_castle_region))
self.world.get_entrance("Goto Consumed King Garden", self.player).connect(consumed_king_garden_region)
self.world.get_entrance("Goto Grand Archives", self.player).connect(grand_archives_region)
consumed_king_garden_region.exits.append(Entrance(self.player, "Goto Untended Graves",
consumed_king_garden_region))
self.world.get_entrance("Goto Untended Graves", self.player).connect(untended_graves_region)
# For each region, add the associated locations retrieved from the corresponding location_table
def create_region(self, region_name, location_table) -> Region:
new_region = Region(region_name, RegionType.Generic, region_name, self.player)
if location_table:
for name, address in location_table.items():
location = DarkSouls3Location(self.player, name, self.location_name_to_id[name], new_region)
new_region.locations.append(location)
self.world.regions.append(new_region)
return new_region
def create_items(self):
for name, address in self.item_name_to_id.items():
# Specific items will be included in the item pool under certain conditions. See generate_basic
if name != "Basin of Vows":
self.world.itempool += [self.create_item(name)]
def generate_early(self):
pass
def set_rules(self) -> None:
# Define the access rules to the entrances
set_rule(self.world.get_entrance("Goto Bell Tower", self.player),
lambda state: state.has("Tower Key", self.player))
set_rule(self.world.get_entrance("Goto Undead Settlement", self.player),
lambda state: state.has("Small Lothric Banner", self.player))
set_rule(self.world.get_entrance("Goto Lothric Castle", self.player),
lambda state: state.has("Basin of Vows", self.player))
set_rule(self.world.get_entrance("Goto Irithyll of the boreal", self.player),
lambda state: state.has("Small Doll", self.player))
set_rule(self.world.get_entrance("Goto Archdragon peak", self.player),
lambda state: state.can_reach("CKG: Soul of Consumed Oceiros", "Location", self.player))
set_rule(self.world.get_entrance("Goto Profaned capital", self.player),
lambda state: state.has("Storm Ruler", self.player))
set_rule(self.world.get_entrance("Goto Grand Archives", self.player),
lambda state: state.has("Grand Archives Key", self.player))
set_rule(self.world.get_entrance("Goto Kiln Of The First Flame", self.player),
lambda state: state.has("Cinders of a Lord - Abyss Watcher", self.player) and
state.has("Cinders of a Lord - Yhorm the Giant", self.player) and
state.has("Cinders of a Lord - Aldrich", self.player) and
state.has("Cinders of a Lord - Lothric Prince", self.player))
# Define the access rules to some specific locations
set_rule(self.world.get_location("HWL: Soul of the Dancer", self.player),
lambda state: state.has("Basin of Vows", self.player))
set_rule(self.world.get_location("HWL: Greirat's Ashes", self.player),
lambda state: state.has("Cell Key", self.player))
set_rule(self.world.get_location("ID: Bellowing Dragoncrest Ring", self.player),
lambda state: state.has("Jailbreaker's Key", self.player))
set_rule(self.world.get_location("ID: Prisoner Chief's Ashes", self.player),
lambda state: state.has("Jailer's Key Ring", self.player))
set_rule(self.world.get_location("ID: Covetous Gold Serpent Ring", self.player),
lambda state: state.has("Old Cell Key", self.player))
black_hand_gotthard_corpse_rule = lambda state: \
(state.can_reach("AL: Cinders of a Lord - Aldrich", "Location", self.player) and
state.can_reach("PC: Cinders of a Lord - Yhorm the Giant", "Location", self.player))
set_rule(self.world.get_location("LC: Grand Archives Key", self.player), black_hand_gotthard_corpse_rule)
set_rule(self.world.get_location("LC: Gotthard Twinswords", self.player), black_hand_gotthard_corpse_rule)
self.world.completion_condition[self.player] = lambda state: \
state.has("Cinders of a Lord - Abyss Watcher", self.player) and \
state.has("Cinders of a Lord - Yhorm the Giant", self.player) and \
state.has("Cinders of a Lord - Aldrich", self.player) and \
state.has("Cinders of a Lord - Lothric Prince", self.player)
def generate_basic(self):
# Depending on the specified option, add the Basin of Vows to a specific location or to the item pool
item = self.create_item("Basin of Vows")
if self.world.late_basin_of_vows[self.player]:
self.world.get_location("IBV: Soul of Pontiff Sulyvahn", self.player).place_locked_item(item)
else:
self.world.itempool += [item]
# Fill item pool with additional items
item_pool_len = self.item_name_to_id.__len__()
total_required_locations = self.location_name_to_id.__len__()
for i in range(item_pool_len, total_required_locations):
self.world.itempool += [self.create_item("Soul of an Intrepid Hero")]
def generate_output(self, output_directory: str):
# Depending on the specified option, modify items hexadecimal value to add an upgrade level
item_dictionary = item_dictionary_table.copy()
if self.world.randomize_weapons_level[self.player]:
# Randomize some weapons upgrades
for name in weapons_upgrade_5_table.keys():
if self.world.random.randint(0, 100) < 33:
value = self.world.random.randint(1, 5)
item_dictionary[name] += value
for name in weapons_upgrade_10_table.keys():
if self.world.random.randint(0, 100) < 33:
value = self.world.random.randint(1, 10)
item_dictionary[name] += value
# Create the mandatory lists to generate the player's output file
items_id = []
items_address = []
locations_id = []
locations_address = []
locations_target = []
for location in self.world.get_filled_locations():
if location.item.player == self.player:
items_id.append(location.item.code)
items_address.append(item_dictionary[location.item.name])
if location.player == self.player:
locations_address.append(location_dictionary_table[location.name])
locations_id.append(location.address)
if location.item.player == self.player:
locations_target.append(item_dictionary[location.item.name])
else:
locations_target.append(0)
data = {
"options": {
"auto_equip": self.world.auto_equip[self.player].value,
"lock_equip": self.world.lock_equip[self.player].value,
"no_weapon_requirements": self.world.no_weapon_requirements[self.player].value,
},
"seed": self.world.seed_name, # to verify the server's multiworld
"slot": self.world.player_name[self.player], # to connect to server
"base_id": self.base_id, # to merge location and items lists
"locationsId": locations_id,
"locationsAddress": locations_address,
"locationsTarget": locations_target,
"itemsId": items_id,
"itemsAddress": items_address
}
# generate the file
filename = f"AP-{self.world.seed_name}-P{self.player}-{self.world.player_name[self.player]}.json"
with open(os.path.join(output_directory, filename), 'w') as outfile:
json.dump(data, outfile)
class DarkSouls3Location(Location):
game: str = "Dark Souls III"
class DarkSouls3Item(Item):
game: str = "Dark Souls III"

View File

@@ -0,0 +1,383 @@
"""
Tools used to create this list :
List of all items https://docs.google.com/spreadsheets/d/1nK2g7g6XJ-qphFAk1tjP3jZtlXWDQY-ItKLa_sniawo/edit#gid=1551945791
Regular expression parser https://regex101.com/r/XdtiLR/2
List of locations https://darksouls3.wiki.fextralife.com/Locations
"""
weapons_upgrade_5_table = {
"Irithyll Straight Sword": 0x0020A760,
"Chaos Blade": 0x004C9960,
"Dragonrider Bow": 0x00D6B0F0,
"White Hair Talisman": 0x00CAF120,
"Izalith Staff": 0x00C96A80,
"Fume Ultra Greatsword": 0x0060E4B0,
"Black Knight Sword": 0x005F5E10,
"Yorshka's Spear": 0x008C3A70,
"Smough's Great Hammer": 0x007E30B0,
"Dragonslayer Greatbow": 0x00CF8500,
"Golden Ritual Spear": 0x00C83200,
"Eleonora": 0x006CCB90,
"Witch's Locks": 0x00B7B740,
"Crystal Chime": 0x00CA2DD0,
"Black Knight Glaive": 0x009AE070,
"Dragonslayer Spear": 0x008CAFA0,
"Caitha's Chime": 0x00CA06C0,
"Sunlight Straight Sword": 0x00203230,
"Firelink Greatsword": 0x0060BDA0,
"Hollowslayer Greatsword": 0x00604870,
"Arstor's Spear": 0x008BEC50,
"Vordt's Great Hammer": 0x007CD120,
"Crystal Sage's Rapier": 0x002E6300,
"Farron Greatsword": 0x005E9AC0,
"Wolf Knight's Greatsword": 0x00602160,
"Dancer's Enchanted Swords": 0x00F4C040,
"Wolnir's Holy Sword": 0x005FFA50,
"Demon's Greataxe": 0x006CA480,
"Demon's Fist": 0x00A84DF0,
"Old King's Great Hammer": 0x007CF830,
"Greatsword of Judgment": 0x005E2590,
"Profaned Greatsword": 0x005E4CA0,
"Yhorm's Great Machete": 0x005F0FF0,
"Cleric's Candlestick": 0x0020F580,
"Dragonslayer Greataxe": 0x006C7D70,
"Moonlight Greatsword": 0x00606F80,
"Gundyr's Halberd": 0x009A1D20,
"Lothric's Holy Sword": 0x005FD340,
"Lorian's Greatsword": 0x005F8520,
"Twin Princes' Greatsword": 0x005FAC30,
"Storm Curved Sword": 0x003E4180,
"Dragonslayer Swordspear": 0x008BC540,
"Sage's Crystal Staff": 0x00C8CE40,
}
weapons_upgrade_10_table = {
"Broken Straight Sword": 0x001EF9B0,
"Deep Battle Axe": 0x0006AFA54,
"Club": 0x007A1200,
"Claymore": 0x005BDBA0,
"Longbow": 0x00D689E0,
"Mail Breaker": 0x002DEDD0,
"Broadsword": 0x001ED2A0,
"Astora's Straight Sword": 0x002191C0,
"Rapier": 0x002E14E0,
"Lucerne": 0x0098BD90,
"Whip": 0x00B71B00,
"Reinforced Club": 0x007A8730,
"Caestus": 0x00A7FFD0,
"Partizan": 0x0089C970,
"Red Hilted Halberd": 0x009AB960,
"Saint's Talisman": 0x00CACA10,
"Large Club": 0x007AFC60,
"Brigand Twindaggers": 0x00F50E60,
"Butcher Knife": 0x006BE130,
"Brigand Axe": 0x006B1DE0,
"Heretic's Staff": 0x00C8F550,
"Great Club": 0x007B4A80,
"Exile Greatsword": 0x005DD770,
"Sellsword Twinblades": 0x00F42400,
"Notched Whip": 0x00B7DE50,
"Astora Greatsword": 0x005C9EF0,
"Executioner's Greatsword": 0x0021DFE0,
"Saint-tree Bellvine": 0x00C9DFB0,
"Saint Bident": 0x008C1360,
"Drang Hammers": 0x00F61FD0,
"Arbalest": 0x00D662D0,
"Sunlight Talisman": 0x00CA54E0,
"Greatsword": 0x005C50D0,
"Black Bow of Pharis": 0x00D7E970,
"Great Axe": 0x006B9310,
"Black Blade": 0x004CC070,
"Blacksmith Hammer": 0x007E57C0,
"Witchtree Branch": 0x00C94370,
"Painting Guardian's Curved Sword": 0x003E6890,
"Pickaxe": 0x007DE290,
"Court Sorcerer's Staff": 0x00C91C60,
"Avelyn": 0x00D6FF10,
"Onikiri and Ubadachi": 0x00F58390,
"Ricard's Rapier": 0x002E3BF0,
"Drakeblood Greatsword": 0x00609690,
"Greatlance": 0x008A8CC0,
"Sniper Crossbow": 0x00D83790,
"Claw": 0x00A7D8C0,
"Drang Twinspears": 0x00F5AAA0,
}
shields_table = {
"East-West Shield": 0x0142B930,
"Silver Eagle Kite Shield": 0x014418C0,
"Small Leather Shield": 0x01315410,
"Blue Wooden Shield": 0x0143F1B0,
"Plank Shield": 0x01346150,
"Caduceus Round Shield": 0x01341330,
"Wargod Wooden Shield": 0x0144DC10,
"Grass Crest Shield": 0x01437C80,
"Golden Falcon Shield": 0x01354BB0,
"Twin Dragon Greatshield": 0x01513820,
"Spider Shield": 0x01435570,
"Crest Shield": 0x01430750,
"Curse Ward Greatshield": 0x01518640,
"Stone Parma": 0x01443FD0,
"Dragon Crest Shield": 0x01432E60,
"Shield of Want": 0x0144B500,
"Black Iron Greatshield": 0x0150EA00,
"Greatshield of Glory": 0x01515F30,
"Sacred Bloom Shield": 0x013572C0,
"Golden Wing Crest Shield": 0x0143CAA0,
"Ancient Dragon Greatshield": 0x013599D0,
"Spirit Tree Crest Shield": 0x014466E0,
}
goods_table = {
"Soul of an Intrepid Hero": 0x4000019D,
"Soul of the Nameless King": 0x400002D2,
"Soul of Champion Gundyr": 0x400002C8,
"Soul of the Twin Princes": 0x400002DB,
"Soul of Consumed Oceiros": 0x400002CE,
"Soul of Aldrich": 0x400002D5,
"Soul of Yhorm the Giant": 0x400002DC,
"Soul of Pontiff Sulyvahn": 0x400002D4,
"Soul of the Old Demon King": 0x400002D0,
"Soul of High Lord Wolnir": 0x400002D6,
"Soul of the Blood of the Wolf": 0x400002CD,
"Soul of the Deacons of the Deep": 0x400002D9,
"Soul of a Crystal Sage": 0x400002CB,
"Soul of Boreal Valley Vordt": 0x400002CF,
"Soul of a Stray Demon": 0x400002E7,
"Soul of a Demon": 0x400002E3,
}
armor_table = {
"Fire Keeper Robe": 0x140D9CE8,
"Fire Keeper Gloves": 0x140DA0D0,
"Fire Keeper Skirt": 0x140DA4B8,
"Deserter Trousers": 0x126265B8,
"Cleric Hat": 0x11D905C0,
"Cleric Blue Robe": 0x11D909A8,
"Cleric Gloves": 0x11D90D90,
"Cleric Trousers": 0x11D91178,
"Northern Helm": 0x116E3600,
"Northern Armor": 0x116E39E8,
"Northern Gloves": 0x116E3DD0,
"Northern Trousers": 0x116E41B8,
"Loincloth": 0x148F57D8,
"Brigand Hood": 0x148009E0,
"Brigand Armor": 0x14800DC8,
"Brigand Gauntlets": 0x148011B0,
"Brigand Trousers": 0x14801598,
"Sorcerer Hood": 0x11C9C380,
"Sorcerer Robe": 0x11C9C768,
"Sorcerer Gloves": 0x11C9CB50,
"Sorcerer Trousers": 0x11C9CF38,
"Fallen Knight Helm": 0x1121EAC0,
"Fallen Knight Armor": 0x1121EEA8,
"Fallen Knight Gauntlets": 0x1121F290,
"Fallen Knight Trousers": 0x1121F678,
"Conjurator Hood": 0x149E8E60,
"Conjurator Robe": 0x149E9248,
"Conjurator Manchettes": 0x149E9630,
"Conjurator Boots": 0x149E9A18,
"Sellsword Helm": 0x11481060,
"Sellsword Armor": 0x11481448,
"Sellsword Gauntlet": 0x11481830,
"Sellsword Trousers": 0x11481C18,
"Herald Helm": 0x114FB180,
"Herald Armor": 0x114FB568,
"Herald Gloves": 0x114FB950,
"Herald Trousers": 0x114FBD38,
"Maiden Hood": 0x14BD12E0,
"Maiden Robe": 0x14BD16C8,
"Maiden Gloves": 0x14BD1AB0,
"Maiden Skirt": 0x14BD1E98,
"Drang Armor": 0x154E0C28,
"Drang Gauntlets": 0x154E1010,
"Drang Shoes": 0x154E13F8,
"Archdeacon White Crown": 0x13EF1480,
"Archdeacon Holy Garb": 0x13EF1868,
"Archdeacon Skirt": 0x13EF2038,
"Antiquated Dress": 0x15D76068,
"Antiquated Gloves": 0x15D76450,
"Antiquated Skirt": 0x15D76838,
"Ragged Mask": 0x148F4C20,
"Crown of Dusk": 0x15D75C80,
"Pharis's Hat": 0x1487AB00,
"Old Sage's Blindfold": 0x11945BA0,
"Painting Guardian Hood": 0x156C8CC0,
"Painting Guardian Gown": 0x156C90A8,
"Painting Guardian Gloves": 0x156C9490,
"Painting Guardian Waistcloth": 0x156C9878,
"Brass Helm": 0x1501BD00,
"Brass Armor": 0x1501C0E8,
"Brass Gauntlets": 0x1501C4D0,
"Brass Leggings": 0x1501C8B8,
"Old Sorcerer Hat": 0x1496ED40,
"Old Sorcerer Coat": 0x1496F128,
"Old Sorcerer Gauntlets": 0x1496F510,
"Old Sorcerer Boots": 0x1496F8F8,
"Court Sorcerer Hood": 0x11BA8140,
"Court Sorcerer Robe": 0x11BA8528,
"Court Sorcerer Gloves": 0x11BA8910,
"Court Sorcerer Trousers": 0x11BA8CF8,
"Dragonslayer Helm": 0x158B1140,
"Dragonslayer Armor": 0x158B1528,
"Dragonslayer Gauntlets": 0x158B1910,
"Dragonslayer Leggings": 0x158B1CF8,
"Hood of Prayer": 0x13AA6A60,
"Robe of Prayer": 0x13AA6E48,
"Skirt of Prayer": 0x13AA7618,
"Winged Knight Helm": 0x12EBAE40,
"Winged Knight Armor": 0x12EBB228,
"Winged Knight Gauntlets": 0x12EBB610,
"Winged Knight Leggings": 0x12EBB9F8,
"Shadow Mask": 0x14D3F640,
"Shadow Garb": 0x14D3FA28,
"Shadow Gauntlets": 0x14D3FE10,
"Shadow Leggings": 0x14D401F8,
}
rings_table = {
"Estus Ring": 0x200050DC,
"Covetous Silver Serpent Ring": 0x20004FB0,
"Fire Clutch Ring": 0x2000501E,
"Flame Stoneplate Ring": 0x20004E52,
"Flynn's Ring": 0x2000503C,
"Chloranthy Ring": 0x20004E2A,
"Morne's Ring": 0x20004F1A,
"Sage Ring": 0x20004F38,
"Aldrich's Sapphire": 0x20005096,
"Lloyd's Sword Ring": 0x200050B4,
"Poisonbite Ring": 0x20004E8E,
"Deep Ring": 0x20004F60,
"Lingering Dragoncrest Ring": 0x20004F2E,
"Carthus Milkring": 0x20004FE2,
"Witch's Ring": 0x20004F11,
"Carthus Bloodring": 0x200050FA,
"Speckled Stoneplate Ring": 0x20004E7A,
"Magic Clutch Ring": 0x2000500A,
"Ring of the Sun's First Born": 0x20004F1B,
"Pontiff's Right Eye": 0x2000510E, "Leo Ring": 0x20004EE8,
"Dark Stoneplate Ring": 0x20004E70,
"Reversal Ring": 0x20005104,
"Ring of Favor": 0x20004E3E,
"Bellowing Dragoncrest Ring": 0x20004F07,
"Covetous Gold Serpent Ring": 0x20004FA6,
"Dusk Crown Ring": 0x20004F4C,
"Dark Clutch Ring": 0x20005028,
"Cursebite Ring": 0x20004E98,
"Sun Princess Ring": 0x20004FBA,
"Aldrich's Ruby": 0x2000508C,
"Scholar Ring": 0x20004EB6,
"Fleshbite Ring": 0x20004EA2,
"Hunter's Ring": 0x20004FF6,
"Ashen Estus Ring": 0x200050E6,
"Hornet Ring": 0x20004F9C,
"Lightning Clutch Ring": 0x20005014,
"Ring of Steel Protection": 0x20004E48,
"Calamity Ring": 0x20005078,
"Thunder Stoneplate Ring": 0x20004E5C,
"Knight's Ring": 0x20004FEC,
"Red Tearstone Ring": 0x20004ECA,
"Dragonscale Ring": 0x2000515E,
"Knight Slayer's Ring": 0x20005000,
}
spells_table = {
"Seek Guidance": 0x40360420,
"Lightning Spear": 0x40362B30,
"Atonement": 0x4039ADA0,
"Great Magic Weapon": 0x40140118,
"Iron Flesh": 0x40251430,
"Lightning Stake": 0x40389C30,
"Toxic Mist": 0x4024F108,
"Sacred Flame": 0x40284880,
"Dorhys' Gnawing": 0x40363EB8,
"Great Heal": 0x40356FB0,
"Lightning Blade": 0x4036C770,
"Profaned Flame": 0x402575D8,
"Wrath of the Gods": 0x4035E0F8,
"Power Within": 0x40253B40,
"Soul Stream": 0x4018B820,
"Divine Pillars of Light": 0x4038C340,
"Great Magic Barrier": 0x40365628,
"Great Magic Shield": 0x40144F38,
}
misc_items_table = {
"Tower Key": 0x400007DF,
"Grave Key": 0x400007D9,
"Cell Key": 0x400007DA,
"Small Lothric Banner": 0x40000836,
"Mortician's Ashes": 0x4000083B,
"Braille Divine Tome of Carim": 0x40000847, # Shop
"Great Swamp Pyromancy Tome": 0x4000084F, # Shop
"Farron Coal ": 0x40000837, # Shop
"Paladin's Ashes": 0x4000083D, #Shop
"Deep Braille Divine Tome": 0x40000860, # Shop
"Small Doll": 0x400007D5,
"Golden Scroll": 0x4000085C,
"Sage's Coal": 0x40000838, # Shop #Unique
"Sage's Scroll": 0x40000854,
"Dreamchaser's Ashes": 0x4000083C, # Shop #Unique
"Cinders of a Lord - Abyss Watcher": 0x4000084B,
"Cinders of a Lord - Yhorm the Giant": 0x4000084D,
"Cinders of a Lord - Aldrich": 0x4000084C,
"Grand Archives Key": 0x400007DE,
"Basin of Vows": 0x40000845,
"Cinders of a Lord - Lothric Prince": 0x4000084E,
"Carthus Pyromancy Tome": 0x40000850,
"Grave Warden's Ashes": 0x4000083E,
"Grave Warden Pyromancy Tome": 0x40000853,
"Quelana Pyromancy Tome": 0x40000852,
"Izalith Pyromancy Tome": 0x40000851,
"Greirat's Ashes": 0x4000083F,
"Excrement-covered Ashes": 0x40000862,
"Easterner's Ashes": 0x40000868,
"Prisoner Chief's Ashes": 0x40000863,
"Jailbreaker's Key": 0x400007D7,
"Dragon Torso Stone": 0x4000017A,
"Profaned Coal": 0x4000083A,
"Xanthous Ashes": 0x40000864,
"Old Cell Key": 0x400007DC,
"Jailer's Key Ring": 0x400007D8,
"Logan's Scroll": 0x40000855,
"Storm Ruler": 0x006132D0,
"Giant's Coal": 0x40000839,
"Coiled Sword Fragment": 0x4000015F,
"Dragon Chaser's Ashes": 0x40000867,
"Twinkling Dragon Torso Stone": 0x40000184,
"Braille Divine Tome of Lothric": 0x40000848,
}
key_items_list = {
"Small Lothric Banner",
"Basin of Vows",
"Small Doll",
"Storm Ruler",
"Grand Archives Key",
"Cinders of a Lord - Abyss Watcher",
"Cinders of a Lord - Yhorm the Giant",
"Cinders of a Lord - Aldrich",
"Cinders of a Lord - Lothric Prince",
"Mortician's Ashes",
"Cell Key",
"Tower Key",
"Jailbreaker's Key",
"Prisoner Chief's Ashes",
"Old Cell Key",
"Jailer's Key Ring",
}
item_dictionary_table = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table, **armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table}

View File

@@ -0,0 +1,434 @@
"""
Tools used to create this list :
List of all items https://docs.google.com/spreadsheets/d/1nK2g7g6XJ-qphFAk1tjP3jZtlXWDQY-ItKLa_sniawo/edit#gid=1551945791
Regular expression parser https://regex101.com/r/XdtiLR/2
List of locations https://darksouls3.wiki.fextralife.com/Locations
"""
cemetery_of_ash_table = {
}
fire_link_shrine_table = {
# "FS: Coiled Sword": 0x40000859, You can still light the Firelink Shrine fire whether you have it or not, useless
"FS: Broken Straight Sword": 0x001EF9B0,
"FS: East-West Shield": 0x0142B930,
"FS: Uchigatana": 0x004C4B40,
"FS: Master's Attire": 0x148F5008,
"FS: Master's Gloves": 0x148F53F0,
}
firelink_shrine_bell_tower_table = {
"FSBT: Covetous Silver Serpent Ring": 0x20004FB0,
"FSBT: Fire Keeper Robe": 0x140D9CE8,
"FSBT: Fire Keeper Gloves": 0x140DA0D0,
"FSBT: Fire Keeper Skirt": 0x140DA4B8,
"FSBT: Estus Ring": 0x200050DC,
"FSBT: Fire Keeper Soul": 0x40000186
}
high_wall_of_lothric = {
"HWL: Deep Battle Axe": 0x0006AFA54,
"HWL: Club": 0x007A1200,
"HWL: Claymore": 0x005BDBA0,
"HWL: Binoculars": 0x40000173,
"HWL: Longbow": 0x00D689E0,
"HWL: Mail Breaker": 0x002DEDD0,
"HWL: Broadsword": 0x001ED2A0,
"HWL: Silver Eagle Kite Shield": 0x014418C0,
"HWL: Astora's Straight Sword": 0x002191C0,
"HWL: Cell Key": 0x400007DA,
"HWL: Rapier": 0x002E14E0,
"HWL: Lucerne": 0x0098BD90,
"HWL: Small Lothric Banner": 0x40000836,
"HWL: Basin of Vows": 0x40000845,
"HWL: Soul of Boreal Valley Vordt": 0x400002CF,
"HWL: Soul of the Dancer": 0x400002CA,
"HWL: Way of Blue Covenant": 0x2000274C,
"HWL: Greirat's Ashes": 0x4000083F,
}
undead_settlement_table = {
"US: Small Leather Shield": 0x01315410,
"US: Whip": 0x00B71B00,
"US: Reinforced Club": 0x007A8730,
"US: Blue Wooden Shield": 0x0143F1B0,
"US: Cleric Hat": 0x11D905C0,
"US: Cleric Blue Robe": 0x11D909A8,
"US: Cleric Gloves": 0x11D90D90,
"US: Cleric Trousers": 0x11D91178,
"US: Mortician's Ashes": 0x4000083B,
"US: Caestus": 0x00A7FFD0,
"US: Plank Shield": 0x01346150,
"US: Flame Stoneplate Ring": 0x20004E52,
"US: Caduceus Round Shield": 0x01341330,
"US: Fire Clutch Ring": 0x2000501E,
"US: Partizan": 0x0089C970,
"US: Bloodbite Ring": 0x20004E84,
"US: Red Hilted Halberd": 0x009AB960,
"US: Saint's Talisman": 0x00CACA10,
"US: Irithyll Straight Sword": 0x0020A760,
"US: Large Club": 0x007AFC60,
"US: Northern Helm": 0x116E3600,
"US: Northern Armor": 0x116E39E8,
"US: Northern Gloves": 0x116E3DD0,
"US: Northern Trousers": 0x116E41B8,
"US: Flynn's Ring": 0x2000503C,
"US: Mirrah Vest": 0x15204568,
"US: Mirrah Gloves": 0x15204950,
"US: Mirrah Trousers": 0x15204D38,
"US: Chloranthy Ring": 0x20004E2A,
"US: Loincloth": 0x148F57D8,
"US: Wargod Wooden Shield": 0x0144DC10,
"US: Loretta's Bone": 0x40000846,
"US: Hand Axe": 0x006ACFC0,
"US: Great Scythe": 0x00989680,
"US: Soul of the Rotted Greatwood": 0x400002D7,
"US: Hawk Ring": 0x20004F92,
"US: Warrior of Sunlight Covenant": 0x20002738,
}
road_of_sacrifice_table = {
"RS: Brigand Twindaggers": 0x00F50E60,
"RS: Brigand Hood": 0x148009E0,
"RS: Brigand Armor": 0x14800DC8,
"RS: Brigand Gauntlets": 0x148011B0,
"RS: Brigand Trousers": 0x14801598,
"RS: Butcher Knife": 0x006BE130,
"RS: Brigand Axe": 0x006B1DE0,
"RS: Braille Divine Tome of Carim": 0x40000847,
"RS: Morne's Ring": 0x20004F1A,
"RS: Twin Dragon Greatshield": 0x01513820,
"RS: Heretic's Staff": 0x00C8F550,
"RS: Sorcerer Hood": 0x11C9C380,
"RS: Sorcerer Robe": 0x11C9C768,
"RS: Sorcerer Gloves": 0x11C9CB50,
"RS: Sorcerer Trousers": 0x11C9CF38,
"RS: Sage Ring": 0x20004F38,
"RS: Fallen Knight Helm": 0x1121EAC0,
"RS: Fallen Knight Armor": 0x1121EEA8,
"RS: Fallen Knight Gauntlets": 0x1121F290,
"RS: Fallen Knight Trousers": 0x1121F678,
"RS: Conjurator Hood": 0x149E8E60,
"RS: Conjurator Robe": 0x149E9248,
"RS: Conjurator Manchettes": 0x149E9630,
"RS: Conjurator Boots": 0x149E9A18,
"RS: Great Swamp Pyromancy Tome": 0x4000084F,
"RS: Great Club": 0x007B4A80,
"RS: Exile Greatsword": 0x005DD770,
"RS: Farron Coal ": 0x40000837,
"RS: Sellsword Twinblades": 0x00F42400,
"RS: Sellsword Helm": 0x11481060,
"RS: Sellsword Armor": 0x11481448,
"RS: Sellsword Gauntlet": 0x11481830,
"RS: Sellsword Trousers": 0x11481C18,
"RS: Golden Falcon Shield": 0x01354BB0,
"RS: Herald Helm": 0x114FB180,
"RS: Herald Armor": 0x114FB568,
"RS: Herald Gloves": 0x114FB950,
"RS: Herald Trousers": 0x114FBD38,
"RS: Grass Crest Shield": 0x01437C80,
"RS: Soul of a Crystal Sage": 0x400002CB,
"RS: Great Swamp Ring": 0x20004F10,
}
cathedral_of_the_deep_table = {
"CD: Paladin's Ashes": 0x4000083D,
"CD: Spider Shield": 0x01435570,
"CD: Crest Shield": 0x01430750,
"CD: Notched Whip": 0x00B7DE50,
"CD: Astora Greatsword": 0x005C9EF0,
"CD: Executioner's Greatsword": 0x0021DFE0,
"CD: Curse Ward Greatshield": 0x01518640,
"CD: Saint-tree Bellvine": 0x00C9DFB0,
"CD: Poisonbite Ring": 0x20004E8E,
"CD: Lloyd's Sword Ring": 0x200050B4,
"CD: Seek Guidance": 0x40360420,
"CD: Aldrich's Sapphire": 0x20005096,
"CD: Deep Braille Divine Tome": 0x40000860,
"CD: Saint Bident": 0x008C1360,
"CD: Maiden Hood": 0x14BD12E0,
"CD: Maiden Robe": 0x14BD16C8,
"CD: Maiden Gloves": 0x14BD1AB0,
"CD: Maiden Skirt": 0x14BD1E98,
"CD: Drang Armor": 0x154E0C28,
"CD: Drang Gauntlets": 0x154E1010,
"CD: Drang Shoes": 0x154E13F8,
"CD: Drang Hammers": 0x00F61FD0,
"CD: Deep Ring": 0x20004F60,
"CD: Archdeacon White Crown": 0x13EF1480,
"CD: Archdeacon Holy Garb": 0x13EF1868,
"CD: Archdeacon Skirt": 0x13EF2038,
"CD: Arbalest": 0x00D662D0,
"CD: Small Doll": 0x400007D5,
"CD: Soul of the Deacons of the Deep": 0x400002D9,
"CD: Rosaria's Fingers Covenant": 0x20002760,
}
farron_keep_table = {
"FK: Ragged Mask": 0x148F4C20,
"FK: Iron Flesh": 0x40251430,
"FK: Golden Scroll": 0x4000085C,
"FK: Antiquated Dress": 0x15D76068,
"FK: Antiquated Gloves": 0x15D76450,
"FK: Antiquated Skirt": 0x15D76838,
"FK: Nameless Knight Helm": 0x143B5FC0,
"FK: Nameless Knight Armor": 0x143B63A8,
"FK: Nameless Knight Gauntlets": 0x143B6790,
"FK: Nameless Knight Leggings": 0x143B6B78,
"FK: Sunlight Talisman": 0x00CA54E0,
"FK: Wolf's Blood Swordgrass": 0x4000016E,
"FK: Greatsword": 0x005C50D0,
"FK: Sage's Coal": 0x40000838,
"FK: Stone Parma": 0x01443FD0,
"FK: Sage's Scroll": 0x40000854,
"FK: Crown of Dusk": 0x15D75C80,
"FK: Lingering Dragoncrest Ring": 0x20004F2E,
"FK: Pharis's Hat": 0x1487AB00,
"FK: Black Bow of Pharis": 0x00D7E970,
"FK: Dreamchaser's Ashes": 0x4000083C,
"FK: Great Axe": 0x006B9310,
"FK: Dragon Crest Shield": 0x01432E60,
"FK: Lightning Spear": 0x40362B30,
"FK: Atonement": 0x4039ADA0,
"FK: Great Magic Weapon": 0x40140118,
"FK: Cinders of a Lord - Abyss Watcher": 0x4000084B,
"FK: Soul of the Blood of the Wolf": 0x400002CD,
"FK: Soul of a Stray Demon": 0x400002E7,
"FK: Watchdogs of Farron Covenant": 0x20002724,
}
catacombs_of_carthus_table = {
"CC: Carthus Pyromancy Tome": 0x40000850,
"CC: Carthus Milkring": 0x20004FE2,
"CC: Grave Warden's Ashes": 0x4000083E,
"CC: Carthus Bloodring": 0x200050FA,
"CC: Grave Warden Pyromancy Tome": 0x40000853,
"CC: Old Sage's Blindfold": 0x11945BA0,
"CC: Witch's Ring": 0x20004F11,
"CC: Black Blade": 0x004CC070,
"CC: Soul of High Lord Wolnir": 0x400002D6,
"CC: Soul of a Demon": 0x400002E3,
}
smouldering_lake_table = {
"SL: Shield of Want": 0x0144B500,
"SL: Speckled Stoneplate Ring": 0x20004E7A,
"SL: Dragonrider Bow": 0x00D6B0F0,
"SL: Lightning Stake": 0x40389C30,
"SL: Izalith Pyromancy Tome": 0x40000851,
"SL: Black Knight Sword": 0x005F5E10,
"SL: Quelana Pyromancy Tome": 0x40000852,
"SL: Toxic Mist": 0x4024F108,
"SL: White Hair Talisman": 0x00CAF120,
"SL: Izalith Staff": 0x00C96A80,
"SL: Sacred Flame": 0x40284880,
"SL: Fume Ultra Greatsword": 0x0060E4B0,
"SL: Black Iron Greatshield": 0x0150EA00,
"SL: Soul of the Old Demon King": 0x400002D0,
"SL: Knight Slayer's Ring": 0x20005000,
}
irithyll_of_the_boreal_valley_table = {
"IBV: Dorhys' Gnawing": 0x40363EB8,
"IBV: Witchtree Branch": 0x00C94370,
"IBV: Magic Clutch Ring": 0x2000500A,
"IBV: Ring of the Sun's First Born": 0x20004F1B,
"IBV: Roster of Knights": 0x4000006C,
"IBV: Pontiff's Right Eye": 0x2000510E,
"IBV: Yorshka's Spear": 0x008C3A70,
"IBV: Great Heal": 0x40356FB0,
"IBV: Smough's Great Hammer": 0x007E30B0,
"IBV: Leo Ring": 0x20004EE8,
"IBV: Excrement-covered Ashes": 0x40000862,
"IBV: Dark Stoneplate Ring": 0x20004E70,
"IBV: Easterner's Ashes": 0x40000868,
"IBV: Painting Guardian's Curved Sword": 0x003E6890,
"IBV: Painting Guardian Hood": 0x156C8CC0,
"IBV: Painting Guardian Gown": 0x156C90A8,
"IBV: Painting Guardian Gloves": 0x156C9490,
"IBV: Painting Guardian Waistcloth": 0x156C9878,
"IBV: Dragonslayer Greatbow": 0x00CF8500,
"IBV: Reversal Ring": 0x20005104,
"IBV: Brass Helm": 0x1501BD00,
"IBV: Brass Armor": 0x1501C0E8,
"IBV: Brass Gauntlets": 0x1501C4D0,
"IBV: Brass Leggings": 0x1501C8B8,
"IBV: Ring of Favor": 0x20004E3E,
"IBV: Golden Ritual Spear": 0x00C83200,
"IBV: Soul of Pontiff Sulyvahn": 0x400002D4,
"IBV: Aldrich Faithful Covenant": 0x2000272E,
"IBV: Drang Twinspears": 0x00F5AAA0,
}
irithyll_dungeon_table = {
"ID: Bellowing Dragoncrest Ring": 0x20004F07,
"ID: Jailbreaker's Key": 0x400007D7,
"ID: Prisoner Chief's Ashes": 0x40000863,
"ID: Old Sorcerer Hat": 0x1496ED40,
"ID: Old Sorcerer Coat": 0x1496F128,
"ID: Old Sorcerer Gauntlets": 0x1496F510,
"ID: Old Sorcerer Boots": 0x1496F8F8,
"ID: Great Magic Shield": 0x40144F38,
"ID: Dragon Torso Stone": 0x4000017A,
"ID: Lightning Blade": 0x4036C770,
"ID: Profaned Coal": 0x4000083A,
"ID: Xanthous Ashes": 0x40000864,
"ID: Old Cell Key": 0x400007DC,
"ID: Pickaxe": 0x007DE290,
"ID: Profaned Flame": 0x402575D8,
"ID: Covetous Gold Serpent Ring": 0x20004FA6,
"ID: Jailer's Key Ring": 0x400007D8,
"ID: Dusk Crown Ring": 0x20004F4C,
"ID: Dark Clutch Ring": 0x20005028,
}
profaned_capital_table = {
"PC: Cursebite Ring": 0x20004E98,
"PC: Court Sorcerer Hood": 0x11BA8140,
"PC: Court Sorcerer Robe": 0x11BA8528,
"PC: Court Sorcerer Gloves": 0x11BA8910,
"PC: Court Sorcerer Trousers": 0x11BA8CF8,
"PC: Wrath of the Gods": 0x4035E0F8,
"PC: Logan's Scroll": 0x40000855,
"PC: Eleonora": 0x006CCB90,
"PC: Court Sorcerer's Staff": 0x00C91C60,
"PC: Greatshield of Glory": 0x01515F30,
"PC: Storm Ruler": 0x006132D0,
"PC: Cinders of a Lord - Yhorm the Giant": 0x4000084D,
"PC: Soul of Yhorm the Giant": 0x400002DC,
}
anor_londo_table = {
"AL: Giant's Coal": 0x40000839,
"AL: Sun Princess Ring": 0x20004FBA,
"AL: Aldrich's Ruby": 0x2000508C,
"AL: Cinders of a Lord - Aldrich": 0x4000084C,
"AL: Soul of Aldrich": 0x400002D5,
}
lothric_castle_table = {
"LC: Hood of Prayer": 0x13AA6A60,
"LC: Robe of Prayer": 0x13AA6E48,
"LC: Skirt of Prayer": 0x13AA7618,
"LC: Sacred Bloom Shield": 0x013572C0,
"LC: Winged Knight Helm": 0x12EBAE40,
"LC: Winged Knight Armor": 0x12EBB228,
"LC: Winged Knight Gauntlets": 0x12EBB610,
"LC: Winged Knight Leggings": 0x12EBB9F8,
"LC: Greatlance": 0x008A8CC0,
"LC: Sniper Crossbow": 0x00D83790,
"LC: Spirit Tree Crest Shield": 0x014466E0,
"LC: Red Tearstone Ring": 0x20004ECA,
"LC: Caitha's Chime": 0x00CA06C0,
"LC: Braille Divine Tome of Lothric": 0x40000848,
"LC: Knight's Ring": 0x20004FEC,
"LC: Sunlight Straight Sword": 0x00203230,
"LC: Soul of Dragonslayer Armour": 0x400002D1,
# The Black Hand Gotthard corpse appears when you have defeated Yhorm and Aldrich and triggered the cutscene
"LC: Grand Archives Key": 0x400007DE, # On Black Hand Gotthard corpse
"LC: Gotthard Twinswords": 0x00F53570 # On Black Hand Gotthard corpse
}
consumed_king_garden_table = {
"CKG: Dragonscale Ring": 0x2000515E,
"CKG: Shadow Mask": 0x14D3F640,
"CKG: Shadow Garb": 0x14D3FA28,
"CKG: Shadow Gauntlets": 0x14D3FE10,
"CKG: Shadow Leggings": 0x14D401F8,
"CKG: Claw": 0x00A7D8C0,
"CKG: Soul of Consumed Oceiros": 0x400002CE,
# "CKG: Path of the Dragon Gesture": 0x40002346, I can't technically randomize it as it is a gesture and not an item
}
grand_archives_table = {
"GA: Avelyn": 0x00D6FF10,
"GA: Witch's Locks": 0x00B7B740,
"GA: Power Within": 0x40253B40,
"GA: Scholar Ring": 0x20004EB6,
"GA: Soul Stream": 0x4018B820,
"GA: Fleshbite Ring": 0x20004EA2,
"GA: Crystal Chime": 0x00CA2DD0,
"GA: Golden Wing Crest Shield": 0x0143CAA0,
"GA: Onikiri and Ubadachi": 0x00F58390,
"GA: Hunter's Ring": 0x20004FF6,
"GA: Divine Pillars of Light": 0x4038C340,
"GA: Cinders of a Lord - Lothric Prince": 0x4000084E,
"GA: Soul of the Twin Princes": 0x400002DB,
"GA: Sage's Crystal Staff": 0x00C8CE40,
}
untended_graves_table = {
"UG: Ashen Estus Ring": 0x200050E6,
"UG: Black Knight Glaive": 0x009AE070,
"UG: Hornet Ring": 0x20004F9C,
"UG: Chaos Blade": 0x004C9960,
"UG: Blacksmith Hammer": 0x007E57C0,
"UG: Eyes of a Fire Keeper": 0x4000085A,
"UG: Coiled Sword Fragment": 0x4000015F,
"UG: Soul of Champion Gundyr": 0x400002C8,
}
archdragon_peak_table = {
"AP: Lightning Clutch Ring": 0x20005014,
"AP: Ancient Dragon Greatshield": 0x013599D0,
"AP: Ring of Steel Protection": 0x20004E48,
"AP: Calamity Ring": 0x20005078,
"AP: Drakeblood Greatsword": 0x00609690,
"AP: Dragonslayer Spear": 0x008CAFA0,
"AP: Thunder Stoneplate Ring": 0x20004E5C,
"AP: Great Magic Barrier": 0x40365628,
"AP: Dragon Chaser's Ashes": 0x40000867,
"AP: Twinkling Dragon Torso Stone": 0x40000184,
"AP: Dragonslayer Helm": 0x158B1140,
"AP: Dragonslayer Armor": 0x158B1528,
"AP: Dragonslayer Gauntlets": 0x158B1910,
"AP: Dragonslayer Leggings": 0x158B1CF8,
"AP: Ricard's Rapier": 0x002E3BF0,
"AP: Soul of the Nameless King": 0x400002D2,
"AP: Dragon Tooth": 0x007E09A0,
"AP: Havel's Greatshield": 0x013376F0,
}
location_dictionary_table = {**cemetery_of_ash_table, **fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table,
**cathedral_of_the_deep_table, **farron_keep_table, **catacombs_of_carthus_table, **smouldering_lake_table, **irithyll_of_the_boreal_valley_table,
**irithyll_dungeon_table, **profaned_capital_table, **anor_londo_table, **lothric_castle_table, **consumed_king_garden_table,
**grand_archives_table, **untended_graves_table, **archdragon_peak_table}

View File

@@ -0,0 +1,22 @@
# Dark Souls III
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
In Dark Souls III, all unique items you can earn from a static corpse, a chest or the death of a Boss/NPC are randomized.
This exclude the upgrade materials such as the titanite shards, the estus shards and the consumables which remain at
the same location. I also added an option available from the settings page to randomize the level of the generated
weapons( from +0 to +10/+5 )
## What Dark Souls III items can appear in other players' worlds?
Every unique items from Dark Souls III can appear in other player's worlds, such as a piece of armor, an upgraded weapon
or a key item.
## What does another world's item look like in Dark Souls III?
In Dark Souls III, items which need to be sent to other worlds appear as a Prism Stone.

View File

@@ -0,0 +1,35 @@
# Dark Souls III Randomizer Setup Guide
## Required Software
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client)
## General Concept
The Dark Souls III AP Client is a dinput8.dll triggered when launching Dark Souls III. This .dll file will launch a command
prompt where you can read information about your run and write any command to interact with the Archipelago server.
The randomization is performed by the AP.json file, an output file generated by the Archipelago server.
## Installation Procedures
**This client has only been tested with the Official Steam version of the game (v1.15/1.35) not matter which DLCs are installed**
Get the dinput8.dll from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client).
Then you need to add the two following files at the root folder of your game
( e.g. "SteamLibrary\steamapps\common\DARK SOULS III\Game" ):
- **dinput8.dll**
- **AP.json** (renamed from the generated file AP-{ROOM_ID}.json)
## Joining a MultiWorld Game
1. Run DarkSoulsIII.exe or run the game through Steam
2. Type in /connect {SERVER_IP}:{SERVER_PORT} in the "Windows Command Prompt" that opened
3. Once connected, create a new game, choose a class and wait for the others before starting
4. You can quit and launch at anytime during a game
## Where do I get a config file?
The [Player Settings](/games/Dark%20Souls%20III/player-settings) page on the website allows you to
configure your personal settings and export them into a config file

221
worlds/dkc3/Client.py Normal file
View File

@@ -0,0 +1,221 @@
import logging
import asyncio
from NetUtils import ClientStatus, color
from SNIClient import Context, snes_buffered_write, snes_flush_writes, snes_read
from Patch import GAME_DKC3
snes_logger = logging.getLogger("SNES")
# DKC3 - DKC3_TODO: Check these values
ROM_START = 0x000000
WRAM_START = 0xF50000
WRAM_SIZE = 0x20000
SRAM_START = 0xE00000
SAVEDATA_START = WRAM_START + 0xF000
SAVEDATA_SIZE = 0x500
DKC3_ROMNAME_START = 0x00FFC0
DKC3_ROMHASH_START = 0x7FC0
ROMNAME_SIZE = 0x15
ROMHASH_SIZE = 0x15
DKC3_RECV_PROGRESS_ADDR = WRAM_START + 0x632 # DKC3_TODO: Find a permanent home for this
DKC3_FILE_NAME_ADDR = WRAM_START + 0x5D9
DEATH_LINK_ACTIVE_ADDR = DKC3_ROMNAME_START + 0x15 # DKC3_TODO: Find a permanent home for this
async def deathlink_kill_player(ctx: Context):
pass
#if ctx.game == GAME_DKC3:
# DKC3_TODO: Handle Receiving Deathlink
async def dkc3_rom_init(ctx: Context):
if not ctx.rom:
ctx.finished_game = False
ctx.death_link_allow_survive = False
game_name = await snes_read(ctx, DKC3_ROMNAME_START, 0x15)
if game_name is None or game_name != b"DONKEY KONG COUNTRY 3":
return False
else:
ctx.game = GAME_DKC3
ctx.items_handling = 0b111 # remote items
rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE)
if rom is None or rom == bytes([0] * ROMHASH_SIZE):
return False
ctx.rom = rom
#death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1)
## DKC3_TODO: Handle Deathlink
#if death_link:
# ctx.allow_collect = bool(death_link[0] & 0b100)
# await ctx.update_death_link(bool(death_link[0] & 0b1))
return True
async def dkc3_game_watcher(ctx: Context):
if ctx.game == GAME_DKC3:
# DKC3_TODO: Handle Deathlink
save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5)
if save_file_name is None or save_file_name[0] == 0x00:
# We haven't loaded a save file
return
new_checks = []
from worlds.dkc3.Rom import location_rom_data, item_rom_data
for loc_id, loc_data in location_rom_data.items():
if loc_id not in ctx.locations_checked:
data = await snes_read(ctx, WRAM_START + loc_data[0], 1)
masked_data = data[0] & (1 << loc_data[1])
bit_set = (masked_data != 0)
invert_bit = ((len(loc_data) >= 3) and loc_data[2])
if bit_set != invert_bit:
# DKC3_TODO: Handle non-included checks
new_checks.append(loc_id)
verify_save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5)
if verify_save_file_name is None or verify_save_file_name[0] == 0x00 or verify_save_file_name != save_file_name:
# We have somehow exited the save file (or worse)
return
rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE)
if rom != ctx.rom:
ctx.rom = None
# We have somehow loaded a different ROM
return
for new_check_id in new_checks:
ctx.locations_checked.add(new_check_id)
location = ctx.location_names[new_check_id]
snes_logger.info(
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
# DKC3_TODO: Make this actually visually display new things received (ASM Hook required)
recv_count = await snes_read(ctx, DKC3_RECV_PROGRESS_ADDR, 1)
recv_index = recv_count[0]
if recv_index < len(ctx.items_received):
item = ctx.items_received[recv_index]
recv_index += 1
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_names[item.item], 'red', 'bold'),
color(ctx.player_names[item.player], 'yellow'),
ctx.location_names[item.location], recv_index, len(ctx.items_received)))
snes_buffered_write(ctx, DKC3_RECV_PROGRESS_ADDR, bytes([recv_index]))
if item.item in item_rom_data:
item_count = await snes_read(ctx, WRAM_START + item_rom_data[item.item][0], 0x1)
new_item_count = item_count[0] + 1
for address in item_rom_data[item.item]:
snes_buffered_write(ctx, WRAM_START + address, bytes([new_item_count]))
# Handle Coin Displays
current_level = await snes_read(ctx, WRAM_START + 0x5E3, 0x5)
overworld_locked = ((await snes_read(ctx, WRAM_START + 0x5FC, 0x1))[0] == 0x01)
if item.item == 0xDC3002 and not overworld_locked and (current_level[0] == 0x0A and current_level[2] == 0x00 and current_level[4] == 0x03):
# Bazaar and Barter
item_count = await snes_read(ctx, WRAM_START + 0xB02, 0x1)
new_item_count = item_count[0] + 1
snes_buffered_write(ctx, WRAM_START + 0xB02, bytes([new_item_count]))
elif item.item == 0xDC3002 and not overworld_locked and current_level[0] == 0x04:
# Swanky
item_count = await snes_read(ctx, WRAM_START + 0xA26, 0x1)
new_item_count = item_count[0] + 1
snes_buffered_write(ctx, WRAM_START + 0xA26, bytes([new_item_count]))
elif item.item == 0xDC3003 and not overworld_locked and (current_level[0] == 0x0A and current_level[2] == 0x08 and current_level[4] == 0x01):
# Boomer
item_count = await snes_read(ctx, WRAM_START + 0xB02, 0x1)
new_item_count = item_count[0] + 1
snes_buffered_write(ctx, WRAM_START + 0xB02, bytes([new_item_count]))
else:
# Handle Patch and Skis
if item.item == 0xDC3007:
num_upgrades = 1
inventory = await snes_read(ctx, WRAM_START + 0x605, 0xF)
if (inventory[0] & 0x02):
num_upgrades = 3
elif (inventory[13] & 0x08) or (inventory[0] & 0x01):
num_upgrades = 2
if num_upgrades == 1:
snes_buffered_write(ctx, WRAM_START + 0x605, bytes([inventory[0] | 0x01]))
if inventory[4] == 0:
snes_buffered_write(ctx, WRAM_START + 0x609, bytes([0x01]))
elif inventory[6] == 0:
snes_buffered_write(ctx, WRAM_START + 0x60B, bytes([0x01]))
elif inventory[8] == 0:
snes_buffered_write(ctx, WRAM_START + 0x60D, bytes([0x01]))
elif inventory[10] == 0:
snes_buffered_write(ctx, WRAM_START + 0x60F, bytes([0x01]))
cove_mekanos_progress = await snes_read(ctx, WRAM_START + 0x691, 0x2)
snes_buffered_write(ctx, WRAM_START + 0x691, bytes([cove_mekanos_progress[0] | 0x01]))
snes_buffered_write(ctx, WRAM_START + 0x692, bytes([cove_mekanos_progress[1] | 0x01]))
elif num_upgrades == 2:
snes_buffered_write(ctx, WRAM_START + 0x605, bytes([inventory[0] | 0x02]))
if inventory[4] == 0:
snes_buffered_write(ctx, WRAM_START + 0x609, bytes([0x02]))
elif inventory[6] == 0:
snes_buffered_write(ctx, WRAM_START + 0x60B, bytes([0x02]))
elif inventory[8] == 0:
snes_buffered_write(ctx, WRAM_START + 0x60D, bytes([0x02]))
elif inventory[10] == 0:
snes_buffered_write(ctx, WRAM_START + 0x60F, bytes([0x02]))
elif num_upgrades == 3:
snes_buffered_write(ctx, WRAM_START + 0x606, bytes([inventory[1] | 0x20]))
k3_ridge_progress = await snes_read(ctx, WRAM_START + 0x693, 0x2)
snes_buffered_write(ctx, WRAM_START + 0x693, bytes([k3_ridge_progress[0] | 0x01]))
snes_buffered_write(ctx, WRAM_START + 0x694, bytes([k3_ridge_progress[1] | 0x01]))
elif item.item == 0xDC3000:
# Handle Victory
if not ctx.finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
else:
print("Item Not Recognized: ", item.item)
pass
await snes_flush_writes(ctx)
# DKC3_TODO: This method of collect should work, however it does not unlock the next level correctly when previous is flagged
# Handle Collected Locations
#for loc_id in ctx.checked_locations:
# if loc_id not in ctx.locations_checked:
# loc_data = location_rom_data[loc_id]
# data = await snes_read(ctx, WRAM_START + loc_data[0], 1)
# invert_bit = ((len(loc_data) >= 3) and loc_data[2])
# if not invert_bit:
# masked_data = data[0] | (1 << loc_data[1])
# print("Collected Location: ", hex(loc_data[0]), " | ", loc_data[1])
# snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data]))
# await snes_flush_writes(ctx)
# else:
# masked_data = data[0] & ~(1 << loc_data[1])
# print("Collected Inverted Location: ", hex(loc_data[0]), " | ", loc_data[1])
# snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data]))
# await snes_flush_writes(ctx)
# ctx.locations_checked.add(loc_id)
# Calculate Boomer Cost Text
boomer_cost_text = await snes_read(ctx, WRAM_START + 0xAAFD, 2)
if boomer_cost_text[0] == 0x31 and boomer_cost_text[1] == 0x35:
boomer_cost = await snes_read(ctx, ROM_START + 0x349857, 1)
boomer_cost_tens = int(boomer_cost[0]) // 10
boomer_cost_ones = int(boomer_cost[0]) % 10
snes_buffered_write(ctx, WRAM_START + 0xAAFD, bytes([0x30 + boomer_cost_tens, 0x30 + boomer_cost_ones]))
await snes_flush_writes(ctx)
boomer_final_cost_text = await snes_read(ctx, WRAM_START + 0xAB9B, 2)
if boomer_final_cost_text[0] == 0x32 and boomer_final_cost_text[1] == 0x35:
boomer_cost = await snes_read(ctx, ROM_START + 0x349857, 1)
boomer_cost_tens = boomer_cost[0] // 10
boomer_cost_ones = boomer_cost[0] % 10
snes_buffered_write(ctx, WRAM_START + 0xAB9B, bytes([0x30 + boomer_cost_tens, 0x30 + boomer_cost_ones]))
await snes_flush_writes(ctx)

52
worlds/dkc3/Items.py Normal file
View File

@@ -0,0 +1,52 @@
import typing
from BaseClasses import Item, ItemClassification
from .Names import ItemName
class ItemData(typing.NamedTuple):
code: typing.Optional[int]
progression: bool
quantity: int = 1
event: bool = False
class DKC3Item(Item):
game: str = "Donkey Kong Country 3"
# Separate tables for each type of item.
junk_table = {
ItemName.one_up_balloon: ItemData(0xDC3001, False),
ItemName.bear_coin: ItemData(0xDC3002, False),
}
collectable_table = {
ItemName.bonus_coin: ItemData(0xDC3003, True),
ItemName.dk_coin: ItemData(0xDC3004, True),
ItemName.banana_bird: ItemData(0xDC3005, True),
ItemName.krematoa_cog: ItemData(0xDC3006, True),
ItemName.progressive_boat: ItemData(0xDC3007, True),
}
inventory_table = {
ItemName.present: ItemData(0xDC3008, True),
ItemName.bowling_ball: ItemData(0xDC3009, True),
ItemName.shell: ItemData(0xDC300A, True),
ItemName.mirror: ItemData(0xDC300B, True),
ItemName.flower: ItemData(0xDC300C, True),
ItemName.wrench: ItemData(0xDC300D, True),
}
event_table = {
ItemName.victory: ItemData(0xDC3000, True),
}
# Complete item table.
item_table = {
**junk_table,
**collectable_table,
**event_table,
}
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}

115
worlds/dkc3/Levels.py Normal file
View File

@@ -0,0 +1,115 @@
from .Names import LocationName
class DKC3Level():
nameIDAddress: int
levelIDAddress: int
nameID: int
levelID: int
def __init__(self, nameIDAddress: int, levelIDAddress: int, nameID: int, levelID: int):
self.nameIDAddress = nameIDAddress
self.levelIDAddress = levelIDAddress
self.nameID = nameID
self.levelID = levelID
level_dict = {
LocationName.lakeside_limbo_region: DKC3Level(0x34D19C, 0x34D19D, 0x01, 0x25),
LocationName.doorstop_dash_region: DKC3Level(0x34D1A7, 0x34D1A8, 0x02, 0x28),
LocationName.tidal_trouble_region: DKC3Level(0x34D1BD, 0x34D1BE, 0x04, 0x27),
LocationName.skiddas_row_region: DKC3Level(0x34D1C8, 0x34D1C9, 0x05, 0x2B),
LocationName.murky_mill_region: DKC3Level(0x34D1D3, 0x34D1D4, 0x0D, 0x2A),
LocationName.barrel_shield_bust_up_region: DKC3Level(0x34D217, 0x34D218, 0x0B, 0x30),
LocationName.riverside_race_region: DKC3Level(0x34D22D, 0x34D22E, 0x0C, 0x32),
LocationName.squeals_on_wheels_region: DKC3Level(0x34D238, 0x34D239, 0x06, 0x29),
LocationName.springin_spiders_region: DKC3Level(0x34D24E, 0x34D24F, 0x0E, 0x2F),
LocationName.bobbing_barrel_brawl_region: DKC3Level(0x34D264, 0x34D265, 0x37, 0x34),
LocationName.bazzas_blockade_region: DKC3Level(0x34D29D, 0x34D29E, 0x14, 0x35),
LocationName.rocket_barrel_ride_region: DKC3Level(0x34D2A8, 0x34D2A9, 0x15, 0x38),
LocationName.kreeping_klasps_region: DKC3Level(0x34D2BE, 0x34D2BF, 0x16, 0x26),
LocationName.tracker_barrel_trek_region: DKC3Level(0x34D2D4, 0x34D2D5, 0x17, 0x39),
LocationName.fish_food_frenzy_region: DKC3Level(0x34D2DF, 0x34D2E0, 0x18, 0x36),
LocationName.fire_ball_frenzy_region: DKC3Level(0x34D30D, 0x34D30E, 0x1B, 0x3B),
LocationName.demolition_drain_pipe_region: DKC3Level(0x34D323, 0x34D324, 0x1D, 0x40),
LocationName.ripsaw_rage_region: DKC3Level(0x34D339, 0x34D33A, 0x1E, 0x2E),
LocationName.blazing_bazookas_region: DKC3Level(0x34D34F, 0x34D350, 0x1F, 0x3C),
LocationName.low_g_labyrinth_region: DKC3Level(0x34D35A, 0x34D35B, 0x20, 0x3E),
LocationName.krevice_kreepers_region: DKC3Level(0x34D388, 0x34D389, 0x23, 0x41),
LocationName.tearaway_toboggan_region: DKC3Level(0x34D393, 0x34D394, 0x24, 0x2D),
LocationName.barrel_drop_bounce_region: DKC3Level(0x34D39E, 0x34D39F, 0x25, 0x3A),
LocationName.krack_shot_kroc_region: DKC3Level(0x34D3A9, 0x34D3AA, 0x26, 0x3D),
LocationName.lemguin_lunge_region: DKC3Level(0x34D3B4, 0x34D3B5, 0x27, 0x2C),
LocationName.buzzer_barrage_region: DKC3Level(0x34D40E, 0x34D40F, 0x2B, 0x44),
LocationName.kong_fused_cliffs_region: DKC3Level(0x34D424, 0x34D425, 0x2D, 0x42),
LocationName.floodlit_fish_region: DKC3Level(0x34D42F, 0x34D430, 0x2E, 0x37),
LocationName.pothole_panic_region: DKC3Level(0x34D43A, 0x34D43B, 0x2F, 0x45),
LocationName.ropey_rumpus_region: DKC3Level(0x34D450, 0x34D451, 0x30, 0x43),
LocationName.konveyor_rope_clash_region: DKC3Level(0x34D489, 0x34D48A, 0x38, 0x48),
LocationName.creepy_caverns_region: DKC3Level(0x34D49F, 0x34D4A0, 0x36, 0x46),
LocationName.lightning_lookout_region: DKC3Level(0x34D4AA, 0x34D4AB, 0x10, 0x33),
LocationName.koindozer_klamber_region: DKC3Level(0x34D4C0, 0x34D4C1, 0x34, 0x47),
LocationName.poisonous_pipeline_region: DKC3Level(0x34D4D6, 0x34D4D7, 0x39, 0x3F),
LocationName.stampede_sprint_region: DKC3Level(0x34D51A, 0x34D51B, 0x3D, 0x49),
LocationName.criss_cross_cliffs_region: DKC3Level(0x34D525, 0x34D526, 0x3E, 0x4A),
LocationName.tyrant_twin_tussle_region: DKC3Level(0x34D530, 0x34D531, 0x3F, 0x4B),
LocationName.swoopy_salvo_region: DKC3Level(0x34D53B, 0x34D53C, 0x40, 0x31),
#LocationName.rocket_rush_region: DKC3Level(0x34D546, 0x34D547, 0x05, 0x4C), # Rocket Rush is not getting shuffled
}
level_list = [
LocationName.lakeside_limbo_region,
LocationName.doorstop_dash_region,
LocationName.tidal_trouble_region,
LocationName.skiddas_row_region,
LocationName.murky_mill_region,
LocationName.barrel_shield_bust_up_region,
LocationName.riverside_race_region,
LocationName.squeals_on_wheels_region,
LocationName.springin_spiders_region,
LocationName.bobbing_barrel_brawl_region,
LocationName.bazzas_blockade_region,
LocationName.rocket_barrel_ride_region,
LocationName.kreeping_klasps_region,
LocationName.tracker_barrel_trek_region,
LocationName.fish_food_frenzy_region,
LocationName.fire_ball_frenzy_region,
LocationName.demolition_drain_pipe_region,
LocationName.ripsaw_rage_region,
LocationName.blazing_bazookas_region,
LocationName.low_g_labyrinth_region,
LocationName.krevice_kreepers_region,
LocationName.tearaway_toboggan_region,
LocationName.barrel_drop_bounce_region,
LocationName.krack_shot_kroc_region,
LocationName.lemguin_lunge_region,
LocationName.buzzer_barrage_region,
LocationName.kong_fused_cliffs_region,
LocationName.floodlit_fish_region,
LocationName.pothole_panic_region,
LocationName.ropey_rumpus_region,
LocationName.konveyor_rope_clash_region,
LocationName.creepy_caverns_region,
LocationName.lightning_lookout_region,
LocationName.koindozer_klamber_region,
LocationName.poisonous_pipeline_region,
LocationName.stampede_sprint_region,
LocationName.criss_cross_cliffs_region,
LocationName.tyrant_twin_tussle_region,
LocationName.swoopy_salvo_region,
#LocationName.rocket_rush_region,
]

283
worlds/dkc3/Locations.py Normal file
View File

@@ -0,0 +1,283 @@
import typing
from BaseClasses import Location
from .Names import LocationName
class DKC3Location(Location):
game: str = "Donkey Kong Country 3"
progress_byte: int = 0x000000
progress_bit: int = 0
inverted_bit: bool = False
def __init__(self, player: int, name: str = '', address: int = None, parent=None, prog_byte: int = None, prog_bit: int = None, invert: bool = False):
super().__init__(player, name, address, parent)
self.progress_byte = prog_byte
self.progress_bit = prog_bit
self.inverted_bit = invert
level_location_table = {
LocationName.lakeside_limbo_flag: 0xDC3000,
LocationName.lakeside_limbo_bonus_1: 0xDC3001,
LocationName.lakeside_limbo_bonus_2: 0xDC3002,
LocationName.lakeside_limbo_dk: 0xDC3003,
LocationName.doorstop_dash_flag: 0xDC3004,
LocationName.doorstop_dash_bonus_1: 0xDC3005,
LocationName.doorstop_dash_bonus_2: 0xDC3006,
LocationName.doorstop_dash_dk: 0xDC3007,
LocationName.tidal_trouble_flag: 0xDC3008,
LocationName.tidal_trouble_bonus_1: 0xDC3009,
LocationName.tidal_trouble_bonus_2: 0xDC300A,
LocationName.tidal_trouble_dk: 0xDC300B,
LocationName.skiddas_row_flag: 0xDC300C,
LocationName.skiddas_row_bonus_1: 0xDC300D,
LocationName.skiddas_row_bonus_2: 0xDC300E,
LocationName.skiddas_row_dk: 0xDC300F,
LocationName.murky_mill_flag: 0xDC3010,
LocationName.murky_mill_bonus_1: 0xDC3011,
LocationName.murky_mill_bonus_2: 0xDC3012,
LocationName.murky_mill_dk: 0xDC3013,
LocationName.barrel_shield_bust_up_flag: 0xDC3014,
LocationName.barrel_shield_bust_up_bonus_1: 0xDC3015,
LocationName.barrel_shield_bust_up_bonus_2: 0xDC3016,
LocationName.barrel_shield_bust_up_dk: 0xDC3017,
LocationName.riverside_race_flag: 0xDC3018,
LocationName.riverside_race_bonus_1: 0xDC3019,
LocationName.riverside_race_bonus_2: 0xDC301A,
LocationName.riverside_race_dk: 0xDC301B,
LocationName.squeals_on_wheels_flag: 0xDC301C,
LocationName.squeals_on_wheels_bonus_1: 0xDC301D,
LocationName.squeals_on_wheels_bonus_2: 0xDC301E,
LocationName.squeals_on_wheels_dk: 0xDC301F,
LocationName.springin_spiders_flag: 0xDC3020,
LocationName.springin_spiders_bonus_1: 0xDC3021,
LocationName.springin_spiders_bonus_2: 0xDC3022,
LocationName.springin_spiders_dk: 0xDC3023,
LocationName.bobbing_barrel_brawl_flag: 0xDC3024,
LocationName.bobbing_barrel_brawl_bonus_1: 0xDC3025,
LocationName.bobbing_barrel_brawl_bonus_2: 0xDC3026,
LocationName.bobbing_barrel_brawl_dk: 0xDC3027,
LocationName.bazzas_blockade_flag: 0xDC3028,
LocationName.bazzas_blockade_bonus_1: 0xDC3029,
LocationName.bazzas_blockade_bonus_2: 0xDC302A,
LocationName.bazzas_blockade_dk: 0xDC302B,
LocationName.rocket_barrel_ride_flag: 0xDC302C,
LocationName.rocket_barrel_ride_bonus_1: 0xDC302D,
LocationName.rocket_barrel_ride_bonus_2: 0xDC302E,
LocationName.rocket_barrel_ride_dk: 0xDC302F,
LocationName.kreeping_klasps_flag: 0xDC3030,
LocationName.kreeping_klasps_bonus_1: 0xDC3031,
LocationName.kreeping_klasps_bonus_2: 0xDC3032,
LocationName.kreeping_klasps_dk: 0xDC3033,
LocationName.tracker_barrel_trek_flag: 0xDC3034,
LocationName.tracker_barrel_trek_bonus_1: 0xDC3035,
LocationName.tracker_barrel_trek_bonus_2: 0xDC3036,
LocationName.tracker_barrel_trek_dk: 0xDC3037,
LocationName.fish_food_frenzy_flag: 0xDC3038,
LocationName.fish_food_frenzy_bonus_1: 0xDC3039,
LocationName.fish_food_frenzy_bonus_2: 0xDC303A,
LocationName.fish_food_frenzy_dk: 0xDC303B,
LocationName.fire_ball_frenzy_flag: 0xDC303C,
LocationName.fire_ball_frenzy_bonus_1: 0xDC303D,
LocationName.fire_ball_frenzy_bonus_2: 0xDC303E,
LocationName.fire_ball_frenzy_dk: 0xDC303F,
LocationName.demolition_drain_pipe_flag: 0xDC3040,
LocationName.demolition_drain_pipe_bonus_1: 0xDC3041,
LocationName.demolition_drain_pipe_bonus_2: 0xDC3042,
LocationName.demolition_drain_pipe_dk: 0xDC3043,
LocationName.ripsaw_rage_flag: 0xDC3044,
LocationName.ripsaw_rage_bonus_1: 0xDC3045,
LocationName.ripsaw_rage_bonus_2: 0xDC3046,
LocationName.ripsaw_rage_dk: 0xDC3047,
LocationName.blazing_bazookas_flag: 0xDC3048,
LocationName.blazing_bazookas_bonus_1: 0xDC3049,
LocationName.blazing_bazookas_bonus_2: 0xDC304A,
LocationName.blazing_bazookas_dk: 0xDC304B,
LocationName.low_g_labyrinth_flag: 0xDC304C,
LocationName.low_g_labyrinth_bonus_1: 0xDC304D,
LocationName.low_g_labyrinth_bonus_2: 0xDC304E,
LocationName.low_g_labyrinth_dk: 0xDC304F,
LocationName.krevice_kreepers_flag: 0xDC3050,
LocationName.krevice_kreepers_bonus_1: 0xDC3051,
LocationName.krevice_kreepers_bonus_2: 0xDC3052,
LocationName.krevice_kreepers_dk: 0xDC3053,
LocationName.tearaway_toboggan_flag: 0xDC3054,
LocationName.tearaway_toboggan_bonus_1: 0xDC3055,
LocationName.tearaway_toboggan_bonus_2: 0xDC3056,
LocationName.tearaway_toboggan_dk: 0xDC3057,
LocationName.barrel_drop_bounce_flag: 0xDC3058,
LocationName.barrel_drop_bounce_bonus_1: 0xDC3059,
LocationName.barrel_drop_bounce_bonus_2: 0xDC305A,
LocationName.barrel_drop_bounce_dk: 0xDC305B,
LocationName.krack_shot_kroc_flag: 0xDC305C,
LocationName.krack_shot_kroc_bonus_1: 0xDC305D,
LocationName.krack_shot_kroc_bonus_2: 0xDC305E,
LocationName.krack_shot_kroc_dk: 0xDC305F,
LocationName.lemguin_lunge_flag: 0xDC3060,
LocationName.lemguin_lunge_bonus_1: 0xDC3061,
LocationName.lemguin_lunge_bonus_2: 0xDC3062,
LocationName.lemguin_lunge_dk: 0xDC3063,
LocationName.buzzer_barrage_flag: 0xDC3064,
LocationName.buzzer_barrage_bonus_1: 0xDC3065,
LocationName.buzzer_barrage_bonus_2: 0xDC3066,
LocationName.buzzer_barrage_dk: 0xDC3067,
LocationName.kong_fused_cliffs_flag: 0xDC3068,
LocationName.kong_fused_cliffs_bonus_1: 0xDC3069,
LocationName.kong_fused_cliffs_bonus_2: 0xDC306A,
LocationName.kong_fused_cliffs_dk: 0xDC306B,
LocationName.floodlit_fish_flag: 0xDC306C,
LocationName.floodlit_fish_bonus_1: 0xDC306D,
LocationName.floodlit_fish_bonus_2: 0xDC306E,
LocationName.floodlit_fish_dk: 0xDC306F,
LocationName.pothole_panic_flag: 0xDC3070,
LocationName.pothole_panic_bonus_1: 0xDC3071,
LocationName.pothole_panic_bonus_2: 0xDC3072,
LocationName.pothole_panic_dk: 0xDC3073,
LocationName.ropey_rumpus_flag: 0xDC3074,
LocationName.ropey_rumpus_bonus_1: 0xDC3075,
LocationName.ropey_rumpus_bonus_2: 0xDC3076,
LocationName.ropey_rumpus_dk: 0xDC3077,
LocationName.konveyor_rope_clash_flag: 0xDC3078,
LocationName.konveyor_rope_clash_bonus_1: 0xDC3079,
LocationName.konveyor_rope_clash_bonus_2: 0xDC307A,
LocationName.konveyor_rope_clash_dk: 0xDC307B,
LocationName.creepy_caverns_flag: 0xDC307C,
LocationName.creepy_caverns_bonus_1: 0xDC307D,
LocationName.creepy_caverns_bonus_2: 0xDC307E,
LocationName.creepy_caverns_dk: 0xDC307F,
LocationName.lightning_lookout_flag: 0xDC3080,
LocationName.lightning_lookout_bonus_1: 0xDC3081,
LocationName.lightning_lookout_bonus_2: 0xDC3082,
LocationName.lightning_lookout_dk: 0xDC3083,
LocationName.koindozer_klamber_flag: 0xDC3084,
LocationName.koindozer_klamber_bonus_1: 0xDC3085,
LocationName.koindozer_klamber_bonus_2: 0xDC3086,
LocationName.koindozer_klamber_dk: 0xDC3087,
LocationName.poisonous_pipeline_flag: 0xDC3088,
LocationName.poisonous_pipeline_bonus_1: 0xDC3089,
LocationName.poisonous_pipeline_bonus_2: 0xDC308A,
LocationName.poisonous_pipeline_dk: 0xDC308B,
LocationName.stampede_sprint_flag: 0xDC308C,
LocationName.stampede_sprint_bonus_1: 0xDC308D,
LocationName.stampede_sprint_bonus_2: 0xDC308E,
LocationName.stampede_sprint_bonus_3: 0xDC308F,
LocationName.stampede_sprint_dk: 0xDC3090,
LocationName.criss_cross_cliffs_flag: 0xDC3091,
LocationName.criss_cross_cliffs_bonus_1: 0xDC3092,
LocationName.criss_cross_cliffs_bonus_2: 0xDC3093,
LocationName.criss_cross_cliffs_dk: 0xDC3094,
LocationName.tyrant_twin_tussle_flag: 0xDC3095,
LocationName.tyrant_twin_tussle_bonus_1: 0xDC3096,
LocationName.tyrant_twin_tussle_bonus_2: 0xDC3097,
LocationName.tyrant_twin_tussle_bonus_3: 0xDC3098,
LocationName.tyrant_twin_tussle_dk: 0xDC3099,
LocationName.swoopy_salvo_flag: 0xDC309A,
LocationName.swoopy_salvo_bonus_1: 0xDC309B,
LocationName.swoopy_salvo_bonus_2: 0xDC309C,
LocationName.swoopy_salvo_bonus_3: 0xDC309D,
LocationName.swoopy_salvo_dk: 0xDC309E,
LocationName.rocket_rush_flag: 0xDC309F,
LocationName.rocket_rush_dk: 0xDC30A0,
}
boss_location_table = {
LocationName.belchas_barn: 0xDC30A1,
LocationName.arichs_ambush: 0xDC30A2,
LocationName.squirts_showdown: 0xDC30A3,
LocationName.kaos_karnage: 0xDC30A4,
LocationName.bleaks_house: 0xDC30A5,
LocationName.barboss_barrier: 0xDC30A6,
LocationName.kastle_kaos: 0xDC30A7,
LocationName.knautilus: 0xDC30A8,
}
secret_cave_location_table = {
LocationName.belchas_burrow: 0xDC30A9,
LocationName.kong_cave: 0xDC30AA,
LocationName.undercover_cove: 0xDC30AB,
LocationName.ks_cache: 0xDC30AC,
LocationName.hill_top_hoard: 0xDC30AD,
LocationName.bounty_beach: 0xDC30AE,
LocationName.smugglers_cove: 0xDC30AF,
LocationName.arichs_hoard: 0xDC30B0,
LocationName.bounty_bay: 0xDC30B1,
LocationName.sky_high_secret: 0xDC30B2,
LocationName.glacial_grotto: 0xDC30B3,
LocationName.cifftop_cache: 0xDC30B4,
LocationName.sewer_stockpile: 0xDC30B5,
LocationName.banana_bird_mother: 0xDC30B6,
}
brothers_bear_location_table = {
LocationName.bazaars_general_store_1: 0xDC30B7,
LocationName.bazaars_general_store_2: 0xDC30B8,
LocationName.brambles_bungalow: 0xDC30B9,
LocationName.flower_spot: 0xDC30BA,
LocationName.barters_swap_shop: 0xDC30BB,
LocationName.barnacles_island: 0xDC30BC,
LocationName.blues_beach_hut: 0xDC30BD,
LocationName.blizzards_basecamp: 0xDC30BE,
}
all_locations = {
**level_location_table,
**boss_location_table,
**secret_cave_location_table,
**brothers_bear_location_table,
}
location_table = {}
def setup_locations(world, player: int):
location_table = {**level_location_table, **boss_location_table, **secret_cave_location_table}
if False:#world.include_trade_sequence[player].value:
location_table.update({**brothers_bear_location_table})
return location_table
lookup_id_to_name: typing.Dict[int, str] = {id: name for name, _ in all_locations.items()}

View File

@@ -0,0 +1,21 @@
# Junk Definitions
one_up_balloon = "1-Up Balloon"
bear_coin = "Bear Coin"
# Collectable Definitions
bonus_coin = "Bonus Coin"
dk_coin = "DK Coin"
banana_bird = "Banana Bird"
krematoa_cog = "Krematoa Cog"
# Inventory Definitions
progressive_boat = "Progressive Boat Upgrade"
present = "Present"
bowling_ball = "Bowling Ball"
shell = "Shell"
mirror = "Mirror"
flower = "Flupperius Petallus Pongus"
wrench = "No. 6 Wrench"
# Other Definitions
victory = "Donkey Kong"

View File

@@ -0,0 +1,336 @@
# Level Definitions
lakeside_limbo_flag = "Lakeside Limbo - Flag"
lakeside_limbo_bonus_1 = "Lakeside Limbo - Bonus 1"
lakeside_limbo_bonus_2 = "Lakeside Limbo - Bonus 2"
lakeside_limbo_dk = "Lakeside Limbo - DK Coin"
doorstop_dash_flag = "Doorstop Dash - Flag"
doorstop_dash_bonus_1 = "Doorstop Dash - Bonus 1"
doorstop_dash_bonus_2 = "Doorstop Dash - Bonus 2"
doorstop_dash_dk = "Doorstop Dash - DK Coin"
tidal_trouble_flag = "Tidal Trouble - Flag"
tidal_trouble_bonus_1 = "Tidal Trouble - Bonus 1"
tidal_trouble_bonus_2 = "Tidal Trouble - Bonus 2"
tidal_trouble_dk = "Tidal Trouble - DK Coin"
skiddas_row_flag = "Skidda's Row - Flag"
skiddas_row_bonus_1 = "Skidda's Row - Bonus 1"
skiddas_row_bonus_2 = "Skidda's Row - Bonus 2"
skiddas_row_dk = "Skidda's Row - DK Coin"
murky_mill_flag = "Murky Mill - Flag"
murky_mill_bonus_1 = "Murky Mill - Bonus 1"
murky_mill_bonus_2 = "Murky Mill - Bonus 2"
murky_mill_dk = "Murky Mill - DK Coin"
barrel_shield_bust_up_flag = "Barrel Shield Bust-Up - Flag"
barrel_shield_bust_up_bonus_1 = "Barrel Shield Bust-Up - Bonus 1"
barrel_shield_bust_up_bonus_2 = "Barrel Shield Bust-Up - Bonus 2"
barrel_shield_bust_up_dk = "Barrel Shield Bust-Up - DK Coin"
riverside_race_flag = "Riverside Race - Flag"
riverside_race_bonus_1 = "Riverside Race - Bonus 1"
riverside_race_bonus_2 = "Riverside Race - Bonus 2"
riverside_race_dk = "Riverside Race - DK Coin"
squeals_on_wheels_flag = "Squeals On Wheels - Flag"
squeals_on_wheels_bonus_1 = "Squeals On Wheels - Bonus 1"
squeals_on_wheels_bonus_2 = "Squeals On Wheels - Bonus 2"
squeals_on_wheels_dk = "Squeals On Wheels - DK Coin"
springin_spiders_flag = "Springin' Spiders - Flag"
springin_spiders_bonus_1 = "Springin' Spiders - Bonus 1"
springin_spiders_bonus_2 = "Springin' Spiders - Bonus 2"
springin_spiders_dk = "Springin' Spiders - DK Coin"
bobbing_barrel_brawl_flag = "Bobbing Barrel Brawl - Flag"
bobbing_barrel_brawl_bonus_1 = "Bobbing Barrel Brawl - Bonus 1"
bobbing_barrel_brawl_bonus_2 = "Bobbing Barrel Brawl - Bonus 2"
bobbing_barrel_brawl_dk = "Bobbing Barrel Brawl - DK Coin"
bazzas_blockade_flag = "Bazza's Blockade - Flag"
bazzas_blockade_bonus_1 = "Bazza's Blockade - Bonus 1"
bazzas_blockade_bonus_2 = "Bazza's Blockade - Bonus 2"
bazzas_blockade_dk = "Bazza's Blockade - DK Coin"
rocket_barrel_ride_flag = "Rocket Barrel Ride - Flag"
rocket_barrel_ride_bonus_1 = "Rocket Barrel Ride - Bonus 1"
rocket_barrel_ride_bonus_2 = "Rocket Barrel Ride - Bonus 2"
rocket_barrel_ride_dk = "Rocket Barrel Ride - DK Coin"
kreeping_klasps_flag = "Kreeping Klasps - Flag"
kreeping_klasps_bonus_1 = "Kreeping Klasps - Bonus 1"
kreeping_klasps_bonus_2 = "Kreeping Klasps - Bonus 2"
kreeping_klasps_dk = "Kreeping Klasps - DK Coin"
tracker_barrel_trek_flag = "Tracker Barrel Trek - Flag"
tracker_barrel_trek_bonus_1 = "Tracker Barrel Trek - Bonus 1"
tracker_barrel_trek_bonus_2 = "Tracker Barrel Trek - Bonus 2"
tracker_barrel_trek_dk = "Tracker Barrel Trek - DK Coin"
fish_food_frenzy_flag = "Fish Food Frenzy - Flag"
fish_food_frenzy_bonus_1 = "Fish Food Frenzy - Bonus 1"
fish_food_frenzy_bonus_2 = "Fish Food Frenzy - Bonus 2"
fish_food_frenzy_dk = "Fish Food Frenzy - DK Coin"
fire_ball_frenzy_flag = "Fire-Ball Frenzy - Flag"
fire_ball_frenzy_bonus_1 = "Fire-Ball Frenzy - Bonus 1"
fire_ball_frenzy_bonus_2 = "Fire-Ball Frenzy - Bonus 2"
fire_ball_frenzy_dk = "Fire-Ball Frenzy - DK Coin"
demolition_drain_pipe_flag = "Demolition Drain-Pipe - Flag"
demolition_drain_pipe_bonus_1 = "Demolition Drain-Pipe - Bonus 1"
demolition_drain_pipe_bonus_2 = "Demolition Drain-Pipe - Bonus 2"
demolition_drain_pipe_dk = "Demolition Drain-Pipe - DK Coin"
ripsaw_rage_flag = "Ripsaw Rage - Flag"
ripsaw_rage_bonus_1 = "Ripsaw Rage - Bonus 1"
ripsaw_rage_bonus_2 = "Ripsaw Rage - Bonus 2"
ripsaw_rage_dk = "Ripsaw Rage - DK Coin"
blazing_bazookas_flag = "Blazing Bazookas - Flag"
blazing_bazookas_bonus_1 = "Blazing Bazookas - Bonus 1"
blazing_bazookas_bonus_2 = "Blazing Bazookas - Bonus 2"
blazing_bazookas_dk = "Blazing Bazookas - DK Coin"
low_g_labyrinth_flag = "Low-G Labyrinth - Flag"
low_g_labyrinth_bonus_1 = "Low-G Labyrinth - Bonus 1"
low_g_labyrinth_bonus_2 = "Low-G Labyrinth - Bonus 2"
low_g_labyrinth_dk = "Low-G Labyrinth - DK Coin"
krevice_kreepers_flag = "Krevice Kreepers - Flag"
krevice_kreepers_bonus_1 = "Krevice Kreepers - Bonus 1"
krevice_kreepers_bonus_2 = "Krevice Kreepers - Bonus 2"
krevice_kreepers_dk = "Krevice Kreepers - DK Coin"
tearaway_toboggan_flag = "Tearaway Toboggan - Flag"
tearaway_toboggan_bonus_1 = "Tearaway Toboggan - Bonus 1"
tearaway_toboggan_bonus_2 = "Tearaway Toboggan - Bonus 2"
tearaway_toboggan_dk = "Tearaway Toboggan - DK Coin"
barrel_drop_bounce_flag = "Barrel Drop Bounce - Flag"
barrel_drop_bounce_bonus_1 = "Barrel Drop Bounce - Bonus 1"
barrel_drop_bounce_bonus_2 = "Barrel Drop Bounce - Bonus 2"
barrel_drop_bounce_dk = "Barrel Drop Bounce - DK Coin"
krack_shot_kroc_flag = "Krack-Shot Kroc - Flag"
krack_shot_kroc_bonus_1 = "Krack-Shot Kroc - Bonus 1"
krack_shot_kroc_bonus_2 = "Krack-Shot Kroc - Bonus 2"
krack_shot_kroc_dk = "Krack-Shot Kroc - DK Coin"
lemguin_lunge_flag = "Lemguin Lunge - Flag"
lemguin_lunge_bonus_1 = "Lemguin Lunge - Bonus 1"
lemguin_lunge_bonus_2 = "Lemguin Lunge - Bonus 2"
lemguin_lunge_dk = "Lemguin Lunge - DK Coin"
buzzer_barrage_flag = "Buzzer Barrage - Flag"
buzzer_barrage_bonus_1 = "Buzzer Barrage - Bonus 1"
buzzer_barrage_bonus_2 = "Buzzer Barrage - Bonus 2"
buzzer_barrage_dk = "Buzzer Barrage - DK Coin"
kong_fused_cliffs_flag = "Kong-Fused Cliffs - Flag"
kong_fused_cliffs_bonus_1 = "Kong-Fused Cliffs - Bonus 1"
kong_fused_cliffs_bonus_2 = "Kong-Fused Cliffs - Bonus 2"
kong_fused_cliffs_dk = "Kong-Fused Cliffs - DK Coin"
floodlit_fish_flag = "Floodlit Fish - Flag"
floodlit_fish_bonus_1 = "Floodlit Fish - Bonus 1"
floodlit_fish_bonus_2 = "Floodlit Fish - Bonus 2"
floodlit_fish_dk = "Floodlit Fish - DK Coin"
pothole_panic_flag = "Pothole Panic - Flag"
pothole_panic_bonus_1 = "Pothole Panic - Bonus 1"
pothole_panic_bonus_2 = "Pothole Panic - Bonus 2"
pothole_panic_dk = "Pothole Panic - DK Coin"
ropey_rumpus_flag = "Ropey Rumpus - Flag"
ropey_rumpus_bonus_1 = "Ropey Rumpus - Bonus 1"
ropey_rumpus_bonus_2 = "Ropey Rumpus - Bonus 2"
ropey_rumpus_dk = "Ropey Rumpus - DK Coin"
konveyor_rope_clash_flag = "Konveyor Rope Klash - Flag"
konveyor_rope_clash_bonus_1 = "Konveyor Rope Klash - Bonus 1"
konveyor_rope_clash_bonus_2 = "Konveyor Rope Klash - Bonus 2"
konveyor_rope_clash_dk = "Konveyor Rope Klash - DK Coin"
creepy_caverns_flag = "Creepy Caverns - Flag"
creepy_caverns_bonus_1 = "Creepy Caverns - Bonus 1"
creepy_caverns_bonus_2 = "Creepy Caverns - Bonus 2"
creepy_caverns_dk = "Creepy Caverns - DK Coin"
lightning_lookout_flag = "Lightning Lookout - Flag"
lightning_lookout_bonus_1 = "Lightning Lookout - Bonus 1"
lightning_lookout_bonus_2 = "Lightning Lookout - Bonus 2"
lightning_lookout_dk = "Lightning Lookout - DK Coin"
koindozer_klamber_flag = "Koindozer Klamber - Flag"
koindozer_klamber_bonus_1 = "Koindozer Klamber - Bonus 1"
koindozer_klamber_bonus_2 = "Koindozer Klamber - Bonus 2"
koindozer_klamber_dk = "Koindozer Klamber - DK Coin"
poisonous_pipeline_flag = "Poisonous Pipeline - Flag"
poisonous_pipeline_bonus_1 = "Poisonous Pipeline - Bonus 1"
poisonous_pipeline_bonus_2 = "Poisonous Pipeline - Bonus 2"
poisonous_pipeline_dk = "Poisonous Pipeline - DK Coin"
stampede_sprint_flag = "Stampede Sprint - Flag"
stampede_sprint_bonus_1 = "Stampede Sprint - Bonus 1"
stampede_sprint_bonus_2 = "Stampede Sprint - Bonus 2"
stampede_sprint_bonus_3 = "Stampede Sprint - Bonus 3"
stampede_sprint_dk = "Stampede Sprint - DK Coin"
criss_cross_cliffs_flag = "Criss Kross Cliffs - Flag"
criss_cross_cliffs_bonus_1 = "Criss Kross Cliffs - Bonus 1"
criss_cross_cliffs_bonus_2 = "Criss Kross Cliffs - Bonus 2"
criss_cross_cliffs_dk = "Criss Kross Cliffs - DK Coin"
tyrant_twin_tussle_flag = "Tyrant Twin Tussle - Flag"
tyrant_twin_tussle_bonus_1 = "Tyrant Twin Tussle - Bonus 1"
tyrant_twin_tussle_bonus_2 = "Tyrant Twin Tussle - Bonus 2"
tyrant_twin_tussle_bonus_3 = "Tyrant Twin Tussle - Bonus 3"
tyrant_twin_tussle_dk = "Tyrant Twin Tussle - DK Coin"
swoopy_salvo_flag = "Swoopy Salvo - Flag"
swoopy_salvo_bonus_1 = "Swoopy Salvo - Bonus 1"
swoopy_salvo_bonus_2 = "Swoopy Salvo - Bonus 2"
swoopy_salvo_bonus_3 = "Swoopy Salvo - Bonus 3"
swoopy_salvo_dk = "Swoopy Salvo - DK Coin"
rocket_rush_flag = "Rocket Rush - Flag"
rocket_rush_dk = "Rocket Rush - DK Coin"
# Boss Definitions
belchas_barn = "Belcha's Barn"
arichs_ambush = "Arich's Ambush"
squirts_showdown = "Squirt's Showdown"
kaos_karnage = "KAOS Karnage"
bleaks_house = "Bleak's House"
barboss_barrier = "Barbos's Barrier"
kastle_kaos = "Kastle KAOS"
knautilus = "Knautilus"
# Banana Bird Cave Definitions
belchas_burrow = "Belcha's Burrow"
kong_cave = "Kong Cave"
undercover_cove = "Undercover Cove"
ks_cache = "K's Cache"
hill_top_hoard = "Hill-Top Hoard"
bounty_beach = "Bounty Beach"
smugglers_cove = "Smuggler's Cove"
arichs_hoard = "Arich's Hoard"
bounty_bay = "Bounty Bay"
sky_high_secret = "Sky-High Secret"
glacial_grotto = "Glacial Grotto"
cifftop_cache = "Clifftop Cache"
sewer_stockpile = "Sewer Stockpile"
banana_bird_mother = "Banana Bird Mother"
# Brothers Bear Definitions
bazaars_general_store_1 = "Bazaar's General Store - 1"
bazaars_general_store_2 = "Bazaar's General Store - 2"
brambles_bungalow = "Bramble's Bungalow"
flower_spot = "Flower Spot"
barters_swap_shop = "Barter's Swap Shop"
barnacles_island = "Barnacle's Island"
blues_beach_hut = "Blue's Beach Hut"
blizzards_basecamp = "Bizzard's Basecamp"
# Region Definitions
menu_region = "Menu"
overworld_1_region = "Overworld 1"
overworld_2_region = "Overworld 2"
overworld_3_region = "Overworld 3"
overworld_4_region = "Overworld 4"
bazaar_region = "Bazaar's General Store Region"
bramble_region = "Bramble's Bungalow Region"
flower_spot_region = "Flower Spot Region"
barter_region = "Barter's Swap Shop Region"
barnacle_region = "Barnacle's Island Region"
blue_region = "Blue's Beach Hut Region"
blizzard_region = "Bizzard's Basecamp Region"
lake_orangatanga_region = "Lake_Orangatanga"
kremwood_forest_region = "Kremwood Forest"
cotton_top_cove_region = "Cotton-Top Cove"
mekanos_region = "Mekanos"
k3_region = "K3"
razor_ridge_region = "Razor Ridge"
kaos_kore_region = "KAOS Kore"
krematoa_region = "Krematoa"
belchas_barn_region = "Belcha's Barn Region"
arichs_ambush_region = "Arich's Ambush Region"
squirts_showdown_region = "Squirt's Showdown Region"
kaos_karnage_region = "KAOS Karnage Region"
bleaks_house_region = "Bleak's House Region"
barboss_barrier_region = "Barbos's Barrier Region"
kastle_kaos_region = "Kastle KAOS Region"
knautilus_region = "Knautilus Region"
belchas_burrow_region = "Belcha's Burrow Region"
kong_cave_region = "Kong Cave Region"
undercover_cove_region = "Undercover Cove Region"
ks_cache_region = "K's Cache Region"
hill_top_hoard_region = "Hill-Top Hoard Region"
bounty_beach_region = "Bounty Beach Region"
smugglers_cove_region = "Smuggler's Cove Region"
arichs_hoard_region = "Arich's Hoard Region"
bounty_bay_region = "Bounty Bay Region"
sky_high_secret_region = "Sky-High Secret Region"
glacial_grotto_region = "Glacial Grotto Region"
cifftop_cache_region = "Clifftop Cache Region"
sewer_stockpile_region = "Sewer Stockpile Region"
lakeside_limbo_region = "Lakeside Limbo"
doorstop_dash_region = "Doorstop Dash"
tidal_trouble_region = "Tidal Trouble"
skiddas_row_region = "Skidda's Row"
murky_mill_region = "Murky Mill"
barrel_shield_bust_up_region = "Barrel Shield Bust-Up"
riverside_race_region = "Riverside Race"
squeals_on_wheels_region = "Squeals On Wheels"
springin_spiders_region = "Springin' Spiders"
bobbing_barrel_brawl_region = "Bobbing Barrel Brawl"
bazzas_blockade_region = "Bazza's Blockade"
rocket_barrel_ride_region = "Rocket Barrel Ride"
kreeping_klasps_region = "Kreeping Klasps"
tracker_barrel_trek_region = "Tracker Barrel Trek"
fish_food_frenzy_region = "Fish Food Frenzy"
fire_ball_frenzy_region = "Fire-Ball Frenzy"
demolition_drain_pipe_region = "Demolition Drain-Pipe"
ripsaw_rage_region = "Ripsaw Rage"
blazing_bazookas_region = "Blazing Bazukas"
low_g_labyrinth_region = "Low-G Labyrinth"
krevice_kreepers_region = "Krevice Kreepers"
tearaway_toboggan_region = "Tearaway Toboggan"
barrel_drop_bounce_region = "Barrel Drop Bounce"
krack_shot_kroc_region = "Krack-Shot Kroc"
lemguin_lunge_region = "Lemguin Lunge"
buzzer_barrage_region = "Buzzer Barrage"
kong_fused_cliffs_region = "Kong-Fused Cliffs"
floodlit_fish_region = "Floodlit Fish"
pothole_panic_region = "Pothole Panic"
ropey_rumpus_region = "Ropey Rumpus"
konveyor_rope_clash_region = "Konveyor Rope Klash"
creepy_caverns_region = "Creepy Caverns"
lightning_lookout_region = "Lightning Lookout"
koindozer_klamber_region = "Koindozer Klamber"
poisonous_pipeline_region = "Poisonous Pipeline"
stampede_sprint_region = "Stampede Sprint"
criss_cross_cliffs_region = "Criss Kross Cliffs"
tyrant_twin_tussle_region = "Tyrant Twin Tussle"
swoopy_salvo_region = "Swoopy Salvo"
rocket_rush_region = "Rocket Rush"

132
worlds/dkc3/Options.py Normal file
View File

@@ -0,0 +1,132 @@
import typing
from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionList
class Goal(Choice):
"""
Determines the goal of the seed
Knautilus: Reach the Knautilus and defeat Baron K. Roolenstein
Banana Bird Hunt: Find a certain number of Banana Birds and rescue their mother
"""
display_name = "Goal"
option_knautilus = 0
option_banana_bird_hunt = 1
default = 0
class IncludeTradeSequence(Toggle):
"""
Allows logic to place items at the various steps of the trade sequence
"""
display_name = "Include Trade Sequence"
class DKCoinsForGyrocopter(Range):
"""
How many DK Coins are needed to unlock the Gyrocopter
Note: Achieving this number before unlocking the Turbo Ski will cause the game to grant you a
one-time upgrade to the next non-unlocked boat, until you return to Funky. Logic does not assume
that you will use this.
"""
display_name = "DK Coins for Gyrocopter"
range_start = 10
range_end = 41
default = 30
class KrematoaBonusCoinCost(Range):
"""
How many Bonus Coins are needed to unlock each level in Krematoa
"""
display_name = "Krematoa Bonus Coins Cost"
range_start = 1
range_end = 17
default = 15
class PercentageOfExtraBonusCoins(Range):
"""
What Percentage of unneeded Bonus Coins are included in the item pool
"""
display_name = "Percentage of Extra Bonus Coins"
range_start = 0
range_end = 100
default = 100
class NumberOfBananaBirds(Range):
"""
How many Banana Birds are put into the item pool
"""
display_name = "Number of Banana Birds"
range_start = 5
range_end = 15
default = 15
class PercentageOfBananaBirds(Range):
"""
What Percentage of Banana Birds in the item pool are required for Banana Bird Hunt
"""
display_name = "Percentage of Banana Birds"
range_start = 20
range_end = 100
default = 100
class LevelShuffle(Toggle):
"""
Whether levels are shuffled
"""
display_name = "Level Shuffle"
class MusicShuffle(Toggle):
"""
Whether music is shuffled
"""
display_name = "Music Shuffle"
class KongPaletteSwap(Choice):
"""
Which Palette to use for the Kongs
"""
display_name = "Kong Palette Swap"
option_default = 0
option_purple = 1
option_spooky = 2
option_dark = 3
option_chocolate = 4
option_shadow = 5
option_red_gold = 6
option_gbc = 7
option_halloween = 8
default = 0
class StartingLifeCount(Range):
"""
How many extra lives to start the game with
"""
display_name = "Starting Life Count"
range_start = 1
range_end = 99
default = 5
dkc3_options: typing.Dict[str, type(Option)] = {
#"death_link": DeathLink, # Disabled
"goal": Goal,
#"include_trade_sequence": IncludeTradeSequence, # Disabled
"dk_coins_for_gyrocopter": DKCoinsForGyrocopter,
"krematoa_bonus_coin_cost": KrematoaBonusCoinCost,
"percentage_of_extra_bonus_coins": PercentageOfExtraBonusCoins,
"number_of_banana_birds": NumberOfBananaBirds,
"percentage_of_banana_birds": PercentageOfBananaBirds,
"level_shuffle": LevelShuffle,
"music_shuffle": MusicShuffle,
"kong_palette_swap": KongPaletteSwap,
"starting_life_count": StartingLifeCount,
}

883
worlds/dkc3/Regions.py Normal file
View File

@@ -0,0 +1,883 @@
import typing
from BaseClasses import MultiWorld, Region, Entrance
from .Items import DKC3Item
from .Locations import DKC3Location
from .Names import LocationName, ItemName
def create_regions(world, player: int, active_locations):
menu_region = create_region(world, player, active_locations, 'Menu', None, None)
overworld_1_region_locations = {}
if world.goal[player] != "knautilus":
overworld_1_region_locations.update({LocationName.banana_bird_mother: []})
overworld_1_region = create_region(world, player, active_locations, LocationName.overworld_1_region,
overworld_1_region_locations, None)
overworld_2_region_locations = {}
overworld_2_region = create_region(world, player, active_locations, LocationName.overworld_2_region,
overworld_2_region_locations, None)
overworld_3_region_locations = {}
overworld_3_region = create_region(world, player, active_locations, LocationName.overworld_3_region,
overworld_3_region_locations, None)
overworld_4_region_locations = {}
overworld_4_region = create_region(world, player, active_locations, LocationName.overworld_4_region,
overworld_4_region_locations, None)
lake_orangatanga_region = create_region(world, player, active_locations, LocationName.lake_orangatanga_region, None, None)
kremwood_forest_region = create_region(world, player, active_locations, LocationName.kremwood_forest_region, None, None)
cotton_top_cove_region = create_region(world, player, active_locations, LocationName.cotton_top_cove_region, None, None)
mekanos_region = create_region(world, player, active_locations, LocationName.mekanos_region, None, None)
k3_region = create_region(world, player, active_locations, LocationName.k3_region, None, None)
razor_ridge_region = create_region(world, player, active_locations, LocationName.razor_ridge_region, None, None)
kaos_kore_region = create_region(world, player, active_locations, LocationName.kaos_kore_region, None, None)
krematoa_region = create_region(world, player, active_locations, LocationName.krematoa_region, None, None)
lakeside_limbo_region_locations = {
LocationName.lakeside_limbo_flag : [0x657, 1],
LocationName.lakeside_limbo_bonus_1 : [0x657, 2],
LocationName.lakeside_limbo_bonus_2 : [0x657, 3],
LocationName.lakeside_limbo_dk : [0x657, 5],
}
lakeside_limbo_region = create_region(world, player, active_locations, LocationName.lakeside_limbo_region,
lakeside_limbo_region_locations, None)
doorstop_dash_region_locations = {
LocationName.doorstop_dash_flag : [0x65A, 1],
LocationName.doorstop_dash_bonus_1 : [0x65A, 2],
LocationName.doorstop_dash_bonus_2 : [0x65A, 3],
LocationName.doorstop_dash_dk : [0x65A, 5],
}
doorstop_dash_region = create_region(world, player, active_locations, LocationName.doorstop_dash_region,
doorstop_dash_region_locations, None)
tidal_trouble_region_locations = {
LocationName.tidal_trouble_flag : [0x659, 1],
LocationName.tidal_trouble_bonus_1 : [0x659, 2],
LocationName.tidal_trouble_bonus_2 : [0x659, 3],
LocationName.tidal_trouble_dk : [0x659, 5],
}
tidal_trouble_region = create_region(world, player, active_locations, LocationName.tidal_trouble_region,
tidal_trouble_region_locations, None)
skiddas_row_region_locations = {
LocationName.skiddas_row_flag : [0x65D, 1],
LocationName.skiddas_row_bonus_1 : [0x65D, 2],
LocationName.skiddas_row_bonus_2 : [0x65D, 3],
LocationName.skiddas_row_dk : [0x65D, 5],
}
skiddas_row_region = create_region(world, player, active_locations, LocationName.skiddas_row_region,
skiddas_row_region_locations, None)
murky_mill_region_locations = {
LocationName.murky_mill_flag : [0x65C, 1],
LocationName.murky_mill_bonus_1 : [0x65C, 2],
LocationName.murky_mill_bonus_2 : [0x65C, 3],
LocationName.murky_mill_dk : [0x65C, 5],
}
murky_mill_region = create_region(world, player, active_locations, LocationName.murky_mill_region,
murky_mill_region_locations, None)
barrel_shield_bust_up_region_locations = {
LocationName.barrel_shield_bust_up_flag : [0x662, 1],
LocationName.barrel_shield_bust_up_bonus_1 : [0x662, 2],
LocationName.barrel_shield_bust_up_bonus_2 : [0x662, 3],
LocationName.barrel_shield_bust_up_dk : [0x662, 5],
}
barrel_shield_bust_up_region = create_region(world, player, active_locations, LocationName.barrel_shield_bust_up_region,
barrel_shield_bust_up_region_locations, None)
riverside_race_region_locations = {
LocationName.riverside_race_flag : [0x664, 1],
LocationName.riverside_race_bonus_1 : [0x664, 2],
LocationName.riverside_race_bonus_2 : [0x664, 3],
LocationName.riverside_race_dk : [0x664, 5],
}
riverside_race_region = create_region(world, player, active_locations, LocationName.riverside_race_region,
riverside_race_region_locations, None)
squeals_on_wheels_region_locations = {
LocationName.squeals_on_wheels_flag : [0x65B, 1],
LocationName.squeals_on_wheels_bonus_1 : [0x65B, 2],
LocationName.squeals_on_wheels_bonus_2 : [0x65B, 3],
LocationName.squeals_on_wheels_dk : [0x65B, 5],
}
squeals_on_wheels_region = create_region(world, player, active_locations, LocationName.squeals_on_wheels_region,
squeals_on_wheels_region_locations, None)
springin_spiders_region_locations = {
LocationName.springin_spiders_flag : [0x661, 1],
LocationName.springin_spiders_bonus_1 : [0x661, 2],
LocationName.springin_spiders_bonus_2 : [0x661, 3],
LocationName.springin_spiders_dk : [0x661, 5],
}
springin_spiders_region = create_region(world, player, active_locations, LocationName.springin_spiders_region,
springin_spiders_region_locations, None)
bobbing_barrel_brawl_region_locations = {
LocationName.bobbing_barrel_brawl_flag : [0x666, 1],
LocationName.bobbing_barrel_brawl_bonus_1 : [0x666, 2],
LocationName.bobbing_barrel_brawl_bonus_2 : [0x666, 3],
LocationName.bobbing_barrel_brawl_dk : [0x666, 5],
}
bobbing_barrel_brawl_region = create_region(world, player, active_locations, LocationName.bobbing_barrel_brawl_region,
bobbing_barrel_brawl_region_locations, None)
bazzas_blockade_region_locations = {
LocationName.bazzas_blockade_flag : [0x667, 1],
LocationName.bazzas_blockade_bonus_1 : [0x667, 2],
LocationName.bazzas_blockade_bonus_2 : [0x667, 3],
LocationName.bazzas_blockade_dk : [0x667, 5],
}
bazzas_blockade_region = create_region(world, player, active_locations, LocationName.bazzas_blockade_region,
bazzas_blockade_region_locations, None)
rocket_barrel_ride_region_locations = {
LocationName.rocket_barrel_ride_flag : [0x66A, 1],
LocationName.rocket_barrel_ride_bonus_1 : [0x66A, 2],
LocationName.rocket_barrel_ride_bonus_2 : [0x66A, 3],
LocationName.rocket_barrel_ride_dk : [0x66A, 5],
}
rocket_barrel_ride_region = create_region(world, player, active_locations, LocationName.rocket_barrel_ride_region,
rocket_barrel_ride_region_locations, None)
kreeping_klasps_region_locations = {
LocationName.kreeping_klasps_flag : [0x658, 1],
LocationName.kreeping_klasps_bonus_1 : [0x658, 2],
LocationName.kreeping_klasps_bonus_2 : [0x658, 3],
LocationName.kreeping_klasps_dk : [0x658, 5],
}
kreeping_klasps_region = create_region(world, player, active_locations, LocationName.kreeping_klasps_region,
kreeping_klasps_region_locations, None)
tracker_barrel_trek_region_locations = {
LocationName.tracker_barrel_trek_flag : [0x66B, 1],
LocationName.tracker_barrel_trek_bonus_1 : [0x66B, 2],
LocationName.tracker_barrel_trek_bonus_2 : [0x66B, 3],
LocationName.tracker_barrel_trek_dk : [0x66B, 5],
}
tracker_barrel_trek_region = create_region(world, player, active_locations, LocationName.tracker_barrel_trek_region,
tracker_barrel_trek_region_locations, None)
fish_food_frenzy_region_locations = {
LocationName.fish_food_frenzy_flag : [0x668, 1],
LocationName.fish_food_frenzy_bonus_1 : [0x668, 2],
LocationName.fish_food_frenzy_bonus_2 : [0x668, 3],
LocationName.fish_food_frenzy_dk : [0x668, 5],
}
fish_food_frenzy_region = create_region(world, player, active_locations, LocationName.fish_food_frenzy_region,
fish_food_frenzy_region_locations, None)
fire_ball_frenzy_region_locations = {
LocationName.fire_ball_frenzy_flag : [0x66D, 1],
LocationName.fire_ball_frenzy_bonus_1 : [0x66D, 2],
LocationName.fire_ball_frenzy_bonus_2 : [0x66D, 3],
LocationName.fire_ball_frenzy_dk : [0x66D, 5],
}
fire_ball_frenzy_region = create_region(world, player, active_locations, LocationName.fire_ball_frenzy_region,
fire_ball_frenzy_region_locations, None)
demolition_drain_pipe_region_locations = {
LocationName.demolition_drain_pipe_flag : [0x672, 1],
LocationName.demolition_drain_pipe_bonus_1 : [0x672, 2],
LocationName.demolition_drain_pipe_bonus_2 : [0x672, 3],
LocationName.demolition_drain_pipe_dk : [0x672, 5],
}
demolition_drain_pipe_region = create_region(world, player, active_locations, LocationName.demolition_drain_pipe_region,
demolition_drain_pipe_region_locations, None)
ripsaw_rage_region_locations = {
LocationName.ripsaw_rage_flag : [0x660, 1],
LocationName.ripsaw_rage_bonus_1 : [0x660, 2],
LocationName.ripsaw_rage_bonus_2 : [0x660, 3],
LocationName.ripsaw_rage_dk : [0x660, 5],
}
ripsaw_rage_region = create_region(world, player, active_locations, LocationName.ripsaw_rage_region,
ripsaw_rage_region_locations, None)
blazing_bazookas_region_locations = {
LocationName.blazing_bazookas_flag : [0x66E, 1],
LocationName.blazing_bazookas_bonus_1 : [0x66E, 2],
LocationName.blazing_bazookas_bonus_2 : [0x66E, 3],
LocationName.blazing_bazookas_dk : [0x66E, 5],
}
blazing_bazookas_region = create_region(world, player, active_locations, LocationName.blazing_bazookas_region,
blazing_bazookas_region_locations, None)
low_g_labyrinth_region_locations = {
LocationName.low_g_labyrinth_flag : [0x670, 1],
LocationName.low_g_labyrinth_bonus_1 : [0x670, 2],
LocationName.low_g_labyrinth_bonus_2 : [0x670, 3],
LocationName.low_g_labyrinth_dk : [0x670, 5],
}
low_g_labyrinth_region = create_region(world, player, active_locations, LocationName.low_g_labyrinth_region,
low_g_labyrinth_region_locations, None)
krevice_kreepers_region_locations = {
LocationName.krevice_kreepers_flag : [0x673, 1],
LocationName.krevice_kreepers_bonus_1 : [0x673, 2],
LocationName.krevice_kreepers_bonus_2 : [0x673, 3],
LocationName.krevice_kreepers_dk : [0x673, 5],
}
krevice_kreepers_region = create_region(world, player, active_locations, LocationName.krevice_kreepers_region,
krevice_kreepers_region_locations, None)
tearaway_toboggan_region_locations = {
LocationName.tearaway_toboggan_flag : [0x65F, 1],
LocationName.tearaway_toboggan_bonus_1 : [0x65F, 2],
LocationName.tearaway_toboggan_bonus_2 : [0x65F, 3],
LocationName.tearaway_toboggan_dk : [0x65F, 5],
}
tearaway_toboggan_region = create_region(world, player, active_locations, LocationName.tearaway_toboggan_region,
tearaway_toboggan_region_locations, None)
barrel_drop_bounce_region_locations = {
LocationName.barrel_drop_bounce_flag : [0x66C, 1],
LocationName.barrel_drop_bounce_bonus_1 : [0x66C, 2],
LocationName.barrel_drop_bounce_bonus_2 : [0x66C, 3],
LocationName.barrel_drop_bounce_dk : [0x66C, 5],
}
barrel_drop_bounce_region = create_region(world, player, active_locations, LocationName.barrel_drop_bounce_region,
barrel_drop_bounce_region_locations, None)
krack_shot_kroc_region_locations = {
LocationName.krack_shot_kroc_flag : [0x66F, 1],
LocationName.krack_shot_kroc_bonus_1 : [0x66F, 2],
LocationName.krack_shot_kroc_bonus_2 : [0x66F, 3],
LocationName.krack_shot_kroc_dk : [0x66F, 5],
}
krack_shot_kroc_region = create_region(world, player, active_locations, LocationName.krack_shot_kroc_region,
krack_shot_kroc_region_locations, None)
lemguin_lunge_region_locations = {
LocationName.lemguin_lunge_flag : [0x65E, 1],
LocationName.lemguin_lunge_bonus_1 : [0x65E, 2],
LocationName.lemguin_lunge_bonus_2 : [0x65E, 3],
LocationName.lemguin_lunge_dk : [0x65E, 5],
}
lemguin_lunge_region = create_region(world, player, active_locations, LocationName.lemguin_lunge_region,
lemguin_lunge_region_locations, None)
buzzer_barrage_region_locations = {
LocationName.buzzer_barrage_flag : [0x676, 1],
LocationName.buzzer_barrage_bonus_1 : [0x676, 2],
LocationName.buzzer_barrage_bonus_2 : [0x676, 3],
LocationName.buzzer_barrage_dk : [0x676, 5],
}
buzzer_barrage_region = create_region(world, player, active_locations, LocationName.buzzer_barrage_region,
buzzer_barrage_region_locations, None)
kong_fused_cliffs_region_locations = {
LocationName.kong_fused_cliffs_flag : [0x674, 1],
LocationName.kong_fused_cliffs_bonus_1 : [0x674, 2],
LocationName.kong_fused_cliffs_bonus_2 : [0x674, 3],
LocationName.kong_fused_cliffs_dk : [0x674, 5],
}
kong_fused_cliffs_region = create_region(world, player, active_locations, LocationName.kong_fused_cliffs_region,
kong_fused_cliffs_region_locations, None)
floodlit_fish_region_locations = {
LocationName.floodlit_fish_flag : [0x669, 1],
LocationName.floodlit_fish_bonus_1 : [0x669, 2],
LocationName.floodlit_fish_bonus_2 : [0x669, 3],
LocationName.floodlit_fish_dk : [0x669, 5],
}
floodlit_fish_region = create_region(world, player, active_locations, LocationName.floodlit_fish_region,
floodlit_fish_region_locations, None)
pothole_panic_region_locations = {
LocationName.pothole_panic_flag : [0x677, 1],
LocationName.pothole_panic_bonus_1 : [0x677, 2],
LocationName.pothole_panic_bonus_2 : [0x677, 3],
LocationName.pothole_panic_dk : [0x677, 5],
}
pothole_panic_region = create_region(world, player, active_locations, LocationName.pothole_panic_region,
pothole_panic_region_locations, None)
ropey_rumpus_region_locations = {
LocationName.ropey_rumpus_flag : [0x675, 1],
LocationName.ropey_rumpus_bonus_1 : [0x675, 2],
LocationName.ropey_rumpus_bonus_2 : [0x675, 3],
LocationName.ropey_rumpus_dk : [0x675, 5],
}
ropey_rumpus_region = create_region(world, player, active_locations, LocationName.ropey_rumpus_region,
ropey_rumpus_region_locations, None)
konveyor_rope_clash_region_locations = {
LocationName.konveyor_rope_clash_flag : [0x657, 1],
LocationName.konveyor_rope_clash_bonus_1 : [0x657, 2],
LocationName.konveyor_rope_clash_bonus_2 : [0x657, 3],
LocationName.konveyor_rope_clash_dk : [0x657, 5],
}
konveyor_rope_clash_region = create_region(world, player, active_locations, LocationName.konveyor_rope_clash_region,
konveyor_rope_clash_region_locations, None)
creepy_caverns_region_locations = {
LocationName.creepy_caverns_flag : [0x678, 1],
LocationName.creepy_caverns_bonus_1 : [0x678, 2],
LocationName.creepy_caverns_bonus_2 : [0x678, 3],
LocationName.creepy_caverns_dk : [0x678, 5],
}
creepy_caverns_region = create_region(world, player, active_locations, LocationName.creepy_caverns_region,
creepy_caverns_region_locations, None)
lightning_lookout_region_locations = {
LocationName.lightning_lookout_flag : [0x665, 1],
LocationName.lightning_lookout_bonus_1 : [0x665, 2],
LocationName.lightning_lookout_bonus_2 : [0x665, 3],
LocationName.lightning_lookout_dk : [0x665, 5],
}
lightning_lookout_region = create_region(world, player, active_locations, LocationName.lightning_lookout_region,
lightning_lookout_region_locations, None)
koindozer_klamber_region_locations = {
LocationName.koindozer_klamber_flag : [0x679, 1],
LocationName.koindozer_klamber_bonus_1 : [0x679, 2],
LocationName.koindozer_klamber_bonus_2 : [0x679, 3],
LocationName.koindozer_klamber_dk : [0x679, 5],
}
koindozer_klamber_region = create_region(world, player, active_locations, LocationName.koindozer_klamber_region,
koindozer_klamber_region_locations, None)
poisonous_pipeline_region_locations = {
LocationName.poisonous_pipeline_flag : [0x671, 1],
LocationName.poisonous_pipeline_bonus_1 : [0x671, 2],
LocationName.poisonous_pipeline_bonus_2 : [0x671, 3],
LocationName.poisonous_pipeline_dk : [0x671, 5],
}
poisonous_pipeline_region = create_region(world, player, active_locations, LocationName.poisonous_pipeline_region,
poisonous_pipeline_region_locations, None)
stampede_sprint_region_locations = {
LocationName.stampede_sprint_flag : [0x67B, 1],
LocationName.stampede_sprint_bonus_1 : [0x67B, 2],
LocationName.stampede_sprint_bonus_2 : [0x67B, 3],
LocationName.stampede_sprint_bonus_3 : [0x67B, 4],
LocationName.stampede_sprint_dk : [0x67B, 5],
}
stampede_sprint_region = create_region(world, player, active_locations, LocationName.stampede_sprint_region,
stampede_sprint_region_locations, None)
criss_cross_cliffs_region_locations = {
LocationName.criss_cross_cliffs_flag : [0x67C, 1],
LocationName.criss_cross_cliffs_bonus_1 : [0x67C, 2],
LocationName.criss_cross_cliffs_bonus_2 : [0x67C, 3],
LocationName.criss_cross_cliffs_dk : [0x67C, 5],
}
criss_cross_cliffs_region = create_region(world, player, active_locations, LocationName.criss_cross_cliffs_region,
criss_cross_cliffs_region_locations, None)
tyrant_twin_tussle_region_locations = {
LocationName.tyrant_twin_tussle_flag : [0x67D, 1],
LocationName.tyrant_twin_tussle_bonus_1 : [0x67D, 2],
LocationName.tyrant_twin_tussle_bonus_2 : [0x67D, 3],
LocationName.tyrant_twin_tussle_bonus_3 : [0x67D, 4],
LocationName.tyrant_twin_tussle_dk : [0x67D, 5],
}
tyrant_twin_tussle_region = create_region(world, player, active_locations, LocationName.tyrant_twin_tussle_region,
tyrant_twin_tussle_region_locations, None)
swoopy_salvo_region_locations = {
LocationName.swoopy_salvo_flag : [0x663, 1],
LocationName.swoopy_salvo_bonus_1 : [0x663, 2],
LocationName.swoopy_salvo_bonus_2 : [0x663, 3],
LocationName.swoopy_salvo_bonus_3 : [0x663, 4],
LocationName.swoopy_salvo_dk : [0x663, 5],
}
swoopy_salvo_region = create_region(world, player, active_locations, LocationName.swoopy_salvo_region,
swoopy_salvo_region_locations, None)
rocket_rush_region_locations = {
LocationName.rocket_rush_flag : [0x67E, 1],
LocationName.rocket_rush_dk : [0x67E, 5],
}
rocket_rush_region = create_region(world, player, active_locations, LocationName.rocket_rush_region,
rocket_rush_region_locations, None)
belchas_barn_region_locations = {
LocationName.belchas_barn: [0x64F, 1],
}
belchas_barn_region = create_region(world, player, active_locations, LocationName.belchas_barn_region,
belchas_barn_region_locations, None)
arichs_ambush_region_locations = {
LocationName.arichs_ambush: [0x650, 1],
}
arichs_ambush_region = create_region(world, player, active_locations, LocationName.arichs_ambush_region,
arichs_ambush_region_locations, None)
squirts_showdown_region_locations = {
LocationName.squirts_showdown: [0x651, 1],
}
squirts_showdown_region = create_region(world, player, active_locations, LocationName.squirts_showdown_region,
squirts_showdown_region_locations, None)
kaos_karnage_region_locations = {
LocationName.kaos_karnage: [0x652, 1],
}
kaos_karnage_region = create_region(world, player, active_locations, LocationName.kaos_karnage_region,
kaos_karnage_region_locations, None)
bleaks_house_region_locations = {
LocationName.bleaks_house: [0x653, 1],
}
bleaks_house_region = create_region(world, player, active_locations, LocationName.bleaks_house_region,
bleaks_house_region_locations, None)
barboss_barrier_region_locations = {
LocationName.barboss_barrier: [0x654, 1],
}
barboss_barrier_region = create_region(world, player, active_locations, LocationName.barboss_barrier_region,
barboss_barrier_region_locations, None)
kastle_kaos_region_locations = {
LocationName.kastle_kaos: [0x655, 1],
}
kastle_kaos_region = create_region(world, player, active_locations, LocationName.kastle_kaos_region,
kastle_kaos_region_locations, None)
knautilus_region_locations = {
LocationName.knautilus: [0x656, 1],
}
knautilus_region = create_region(world, player, active_locations, LocationName.knautilus_region,
knautilus_region_locations, None)
belchas_burrow_region_locations = {
LocationName.belchas_burrow: [0x647, 1],
}
belchas_burrow_region = create_region(world, player, active_locations, LocationName.belchas_burrow_region,
belchas_burrow_region_locations, None)
kong_cave_region_locations = {
LocationName.kong_cave: [0x645, 1],
}
kong_cave_region = create_region(world, player, active_locations, LocationName.kong_cave_region,
kong_cave_region_locations, None)
undercover_cove_region_locations = {
LocationName.undercover_cove: [0x644, 1],
}
undercover_cove_region = create_region(world, player, active_locations, LocationName.undercover_cove_region,
undercover_cove_region_locations, None)
ks_cache_region_locations = {
LocationName.ks_cache: [0x642, 1],
}
ks_cache_region = create_region(world, player, active_locations, LocationName.ks_cache_region,
ks_cache_region_locations, None)
hill_top_hoard_region_locations = {
LocationName.hill_top_hoard: [0x643, 1],
}
hill_top_hoard_region = create_region(world, player, active_locations, LocationName.hill_top_hoard_region,
hill_top_hoard_region_locations, None)
bounty_beach_region_locations = {
LocationName.bounty_beach: [0x646, 1],
}
bounty_beach_region = create_region(world, player, active_locations, LocationName.bounty_beach_region,
bounty_beach_region_locations, None)
smugglers_cove_region_locations = {
LocationName.smugglers_cove: [0x648, 1],
}
smugglers_cove_region = create_region(world, player, active_locations, LocationName.smugglers_cove_region,
smugglers_cove_region_locations, None)
arichs_hoard_region_locations = {
LocationName.arichs_hoard: [0x649, 1],
}
arichs_hoard_region = create_region(world, player, active_locations, LocationName.arichs_hoard_region,
arichs_hoard_region_locations, None)
bounty_bay_region_locations = {
LocationName.bounty_bay: [0x64A, 1],
}
bounty_bay_region = create_region(world, player, active_locations, LocationName.bounty_bay_region,
bounty_bay_region_locations, None)
sky_high_secret_region_locations = {}
if False:#world.include_trade_sequence[player]:
sky_high_secret_region_locations.update({
LocationName.sky_high_secret: [0x64B, 1],
})
sky_high_secret_region = create_region(world, player, active_locations, LocationName.sky_high_secret_region,
sky_high_secret_region_locations, None)
glacial_grotto_region_locations = {
LocationName.glacial_grotto: [0x64C, 1],
}
glacial_grotto_region = create_region(world, player, active_locations, LocationName.glacial_grotto_region,
glacial_grotto_region_locations, None)
cifftop_cache_region_locations = {}
if False:#world.include_trade_sequence[player]:
cifftop_cache_region_locations.update({
LocationName.cifftop_cache: [0x64D, 1],
})
cifftop_cache_region = create_region(world, player, active_locations, LocationName.cifftop_cache_region,
cifftop_cache_region_locations, None)
sewer_stockpile_region_locations = {
LocationName.sewer_stockpile: [0x64E, 1],
}
sewer_stockpile_region = create_region(world, player, active_locations, LocationName.sewer_stockpile_region,
sewer_stockpile_region_locations, None)
# Set up the regions correctly.
world.regions += [
menu_region,
overworld_1_region,
overworld_2_region,
overworld_3_region,
overworld_4_region,
lake_orangatanga_region,
kremwood_forest_region,
cotton_top_cove_region,
mekanos_region,
k3_region,
razor_ridge_region,
kaos_kore_region,
krematoa_region,
lakeside_limbo_region,
doorstop_dash_region,
tidal_trouble_region,
skiddas_row_region,
murky_mill_region,
barrel_shield_bust_up_region,
riverside_race_region,
squeals_on_wheels_region,
springin_spiders_region,
bobbing_barrel_brawl_region,
bazzas_blockade_region,
rocket_barrel_ride_region,
kreeping_klasps_region,
tracker_barrel_trek_region,
fish_food_frenzy_region,
fire_ball_frenzy_region,
demolition_drain_pipe_region,
ripsaw_rage_region,
blazing_bazookas_region,
low_g_labyrinth_region,
krevice_kreepers_region,
tearaway_toboggan_region,
barrel_drop_bounce_region,
krack_shot_kroc_region,
lemguin_lunge_region,
buzzer_barrage_region,
kong_fused_cliffs_region,
floodlit_fish_region,
pothole_panic_region,
ropey_rumpus_region,
konveyor_rope_clash_region,
creepy_caverns_region,
lightning_lookout_region,
koindozer_klamber_region,
poisonous_pipeline_region,
stampede_sprint_region,
criss_cross_cliffs_region,
tyrant_twin_tussle_region,
swoopy_salvo_region,
rocket_rush_region,
belchas_barn_region,
arichs_ambush_region,
squirts_showdown_region,
kaos_karnage_region,
bleaks_house_region,
barboss_barrier_region,
kastle_kaos_region,
knautilus_region,
belchas_burrow_region,
kong_cave_region,
undercover_cove_region,
ks_cache_region,
hill_top_hoard_region,
bounty_beach_region,
smugglers_cove_region,
arichs_hoard_region,
bounty_bay_region,
sky_high_secret_region,
glacial_grotto_region,
cifftop_cache_region,
sewer_stockpile_region,
]
bazaar_region_locations = {}
bramble_region_locations = {}
flower_spot_region_locations = {}
barter_region_locations = {}
barnacle_region_locations = {}
blue_region_locations = {}
blizzard_region_locations = {}
if False:#world.include_trade_sequence[player]:
bazaar_region_locations.update({
LocationName.bazaars_general_store_1: [0x615, 2, True],
LocationName.bazaars_general_store_2: [0x615, 3, True],
})
bramble_region_locations.update({
LocationName.brambles_bungalow: [0x619, 2],
})
#flower_spot_region_locations.update({
# LocationName.flower_spot: [0x615, 3, True],
#})
barter_region_locations.update({
LocationName.barters_swap_shop: [0x61B, 3],
})
barnacle_region_locations.update({
LocationName.barnacles_island: [0x61D, 2],
})
blue_region_locations.update({
LocationName.blues_beach_hut: [0x621, 4],
})
blizzard_region_locations.update({
LocationName.blizzards_basecamp: [0x625, 4, True],
})
bazaar_region = create_region(world, player, active_locations, LocationName.bazaar_region,
bazaar_region_locations, None)
bramble_region = create_region(world, player, active_locations, LocationName.bramble_region,
bramble_region_locations, None)
flower_spot_region = create_region(world, player, active_locations, LocationName.flower_spot_region,
flower_spot_region_locations, None)
barter_region = create_region(world, player, active_locations, LocationName.barter_region,
barter_region_locations, None)
barnacle_region = create_region(world, player, active_locations, LocationName.barnacle_region,
barnacle_region_locations, None)
blue_region = create_region(world, player, active_locations, LocationName.blue_region,
blue_region_locations, None)
blizzard_region = create_region(world, player, active_locations, LocationName.blizzard_region,
blizzard_region_locations, None)
world.regions += [
bazaar_region,
bramble_region,
flower_spot_region,
barter_region,
barnacle_region,
blue_region,
blizzard_region,
]
def connect_regions(world, player, level_list):
names: typing.Dict[str, int] = {}
# Overworld
connect(world, player, names, 'Menu', LocationName.overworld_1_region)
connect(world, player, names, LocationName.overworld_1_region, LocationName.overworld_2_region,
lambda state: (state.has(ItemName.progressive_boat, player, 1)))
connect(world, player, names, LocationName.overworld_2_region, LocationName.overworld_3_region,
lambda state: (state.has(ItemName.progressive_boat, player, 3)))
connect(world, player, names, LocationName.overworld_1_region, LocationName.overworld_4_region,
lambda state: (state.has(ItemName.dk_coin, player, world.dk_coins_for_gyrocopter[player].value) and
state.has(ItemName.progressive_boat, player, 3)))
# World Connections
connect(world, player, names, LocationName.overworld_1_region, LocationName.lake_orangatanga_region)
connect(world, player, names, LocationName.overworld_1_region, LocationName.kremwood_forest_region)
connect(world, player, names, LocationName.overworld_1_region, LocationName.bounty_beach_region)
connect(world, player, names, LocationName.overworld_1_region, LocationName.bazaar_region)
connect(world, player, names, LocationName.overworld_2_region, LocationName.cotton_top_cove_region)
connect(world, player, names, LocationName.overworld_2_region, LocationName.mekanos_region)
connect(world, player, names, LocationName.overworld_2_region, LocationName.kong_cave_region)
connect(world, player, names, LocationName.overworld_2_region, LocationName.bramble_region)
connect(world, player, names, LocationName.overworld_3_region, LocationName.k3_region)
connect(world, player, names, LocationName.overworld_3_region, LocationName.razor_ridge_region)
connect(world, player, names, LocationName.overworld_3_region, LocationName.kaos_kore_region)
connect(world, player, names, LocationName.overworld_3_region, LocationName.krematoa_region)
connect(world, player, names, LocationName.overworld_3_region, LocationName.undercover_cove_region)
connect(world, player, names, LocationName.overworld_3_region, LocationName.flower_spot_region)
connect(world, player, names, LocationName.overworld_3_region, LocationName.barter_region)
connect(world, player, names, LocationName.overworld_4_region, LocationName.belchas_burrow_region)
connect(world, player, names, LocationName.overworld_4_region, LocationName.ks_cache_region)
connect(world, player, names, LocationName.overworld_4_region, LocationName.hill_top_hoard_region)
# Lake Orangatanga Connections
lake_orangatanga_levels = [
level_list[0],
level_list[1],
level_list[2],
level_list[3],
level_list[4],
LocationName.belchas_barn_region,
LocationName.barnacle_region,
LocationName.smugglers_cove_region,
]
for i in range(0, len(lake_orangatanga_levels)):
connect(world, player, names, LocationName.lake_orangatanga_region, lake_orangatanga_levels[i])
# Kremwood Forest Connections
kremwood_forest_levels = [
level_list[5],
level_list[6],
level_list[7],
level_list[8],
level_list[9],
LocationName.arichs_ambush_region,
LocationName.arichs_hoard_region,
]
for i in range(0, len(kremwood_forest_levels) - 1):
connect(world, player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[i])
connect(world, player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[-1],
lambda state: (state.can_reach(LocationName.riverside_race_flag, "Location", player)))
# Cotton-Top Cove Connections
cotton_top_cove_levels = [
LocationName.blue_region,
level_list[10],
level_list[11],
level_list[12],
level_list[13],
level_list[14],
LocationName.squirts_showdown_region,
LocationName.bounty_bay_region,
]
for i in range(0, len(cotton_top_cove_levels)):
connect(world, player, names, LocationName.cotton_top_cove_region, cotton_top_cove_levels[i])
# Mekanos Connections
mekanos_levels = [
level_list[15],
level_list[16],
level_list[17],
level_list[18],
level_list[19],
LocationName.kaos_karnage_region,
]
for i in range(0, len(mekanos_levels)):
connect(world, player, names, LocationName.mekanos_region, mekanos_levels[i])
if False:#world.include_trade_sequence[player]:
connect(world, player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region,
lambda state: (state.has(ItemName.bowling_ball, player, 1)))
else:
connect(world, player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region,
lambda state: (state.can_reach(LocationName.bleaks_house, "Location", player)))
# K3 Connections
k3_levels = [
level_list[20],
level_list[21],
level_list[22],
level_list[23],
level_list[24],
LocationName.bleaks_house_region,
LocationName.blizzard_region,
LocationName.glacial_grotto_region,
]
for i in range(0, len(k3_levels)):
connect(world, player, names, LocationName.k3_region, k3_levels[i])
# Razor Ridge Connections
razor_ridge_levels = [
level_list[25],
level_list[26],
level_list[27],
level_list[28],
level_list[29],
LocationName.barboss_barrier_region,
]
for i in range(0, len(razor_ridge_levels)):
connect(world, player, names, LocationName.razor_ridge_region, razor_ridge_levels[i])
if False:#world.include_trade_sequence[player]:
connect(world, player, names, LocationName.razor_ridge_region, LocationName.cifftop_cache_region,
lambda state: (state.has(ItemName.wrench, player, 1)))
else:
connect(world, player, names, LocationName.razor_ridge_region, LocationName.cifftop_cache_region)
# KAOS Kore Connections
kaos_kore_levels = [
level_list[30],
level_list[31],
level_list[32],
level_list[33],
level_list[34],
LocationName.kastle_kaos_region,
LocationName.sewer_stockpile_region,
]
for i in range(0, len(kaos_kore_levels)):
connect(world, player, names, LocationName.kaos_kore_region, kaos_kore_levels[i])
# Krematoa Connections
krematoa_levels = [
level_list[35],
level_list[36],
level_list[37],
level_list[38],
LocationName.rocket_rush_region,
]
for i in range(0, len(krematoa_levels)):
connect(world, player, names, LocationName.krematoa_region, krematoa_levels[i],
lambda state: (state.has(ItemName.bonus_coin, player, world.krematoa_bonus_coin_cost[player].value * (i+1))))
connect(world, player, names, LocationName.krematoa_region, LocationName.knautilus_region,
lambda state: (state.has(ItemName.krematoa_cog, player, 5)))
def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None, exits=None):
# Shamelessly stolen from the ROR2 definition
ret = Region(name, None, name, player)
ret.world = world
if locations:
for locationName, locationData in locations.items():
loc_id = active_locations.get(locationName, 0)
if loc_id:
loc_byte = locationData[0] if (len(locationData) > 0) else 0
loc_bit = locationData[1] if (len(locationData) > 1) else 0
loc_invert = locationData[2] if (len(locationData) > 2) else False
location = DKC3Location(player, locationName, loc_id, ret, loc_byte, loc_bit, loc_invert)
ret.locations.append(location)
if exits:
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
return ret
def connect(world: MultiWorld, player: int, used_names: typing.Dict[str, int], source: str, target: str,
rule: typing.Optional[typing.Callable] = None):
source_region = world.get_region(source, player)
target_region = world.get_region(target, player)
if target not in used_names:
used_names[target] = 1
name = target
else:
used_names[target] += 1
name = target + (' ' * used_names[target])
connection = Entrance(player, name, source_region)
if rule:
connection.access_rule = rule
source_region.exits.append(connection)
connection.connect(target_region)

562
worlds/dkc3/Rom.py Normal file
View File

@@ -0,0 +1,562 @@
import Utils
from Patch import read_rom, APDeltaPatch
from .Locations import lookup_id_to_name, all_locations
from .Levels import level_list, level_dict
USHASH = '120abf304f0c40fe059f6a192ed4f947'
ROM_PLAYER_LIMIT = 65535
import hashlib
import os
import math
location_rom_data = {
0xDC3000: [0x657, 1], # Lakeside Limbo
0xDC3001: [0x657, 2],
0xDC3002: [0x657, 3],
0xDC3003: [0x657, 5],
0xDC3004: [0x65A, 1], # Doorstop Dash
0xDC3005: [0x65A, 2],
0xDC3006: [0x65A, 3],
0xDC3007: [0x65A, 5],
0xDC3008: [0x659, 1], # Tidal Trouble
0xDC3009: [0x659, 2],
0xDC300A: [0x659, 3],
0xDC300B: [0x659, 5],
0xDC300C: [0x65D, 1], # Skidda's Row
0xDC300D: [0x65D, 2],
0xDC300E: [0x65D, 3],
0xDC300F: [0x65D, 5],
0xDC3010: [0x65C, 1], # Murky Mill
0xDC3011: [0x65C, 2],
0xDC3012: [0x65C, 3],
0xDC3013: [0x65C, 5],
0xDC3014: [0x662, 1], # Barrel Shield Bust-Up
0xDC3015: [0x662, 2],
0xDC3016: [0x662, 3],
0xDC3017: [0x662, 5],
0xDC3018: [0x664, 1], # Riverside Race
0xDC3019: [0x664, 2],
0xDC301A: [0x664, 3],
0xDC301B: [0x664, 5],
0xDC301C: [0x65B, 1], # Squeals on Wheels
0xDC301D: [0x65B, 2],
0xDC301E: [0x65B, 3],
0xDC301F: [0x65B, 5],
0xDC3020: [0x661, 1], # Springin' Spiders
0xDC3021: [0x661, 2],
0xDC3022: [0x661, 3],
0xDC3023: [0x661, 5],
0xDC3024: [0x666, 1], # Bobbing Barrel Brawl
0xDC3025: [0x666, 2],
0xDC3026: [0x666, 3],
0xDC3027: [0x666, 5],
0xDC3028: [0x667, 1], # Bazza's Blockade
0xDC3029: [0x667, 2],
0xDC302A: [0x667, 3],
0xDC302B: [0x667, 5],
0xDC302C: [0x66A, 1], # Rocket Barrel Ride
0xDC302D: [0x66A, 2],
0xDC302E: [0x66A, 3],
0xDC302F: [0x66A, 5],
0xDC3030: [0x658, 1], # Kreeping Klasps
0xDC3031: [0x658, 2],
0xDC3032: [0x658, 3],
0xDC3033: [0x658, 5],
0xDC3034: [0x66B, 1], # Tracker Barrel Trek
0xDC3035: [0x66B, 2],
0xDC3036: [0x66B, 3],
0xDC3037: [0x66B, 5],
0xDC3038: [0x668, 1], # Fish Food Frenzy
0xDC3039: [0x668, 2],
0xDC303A: [0x668, 3],
0xDC303B: [0x668, 5],
0xDC303C: [0x66D, 1], # Fire-ball Frenzy
0xDC303D: [0x66D, 2],
0xDC303E: [0x66D, 3],
0xDC303F: [0x66D, 5],
0xDC3040: [0x672, 1], # Demolition Drainpipe
0xDC3041: [0x672, 2],
0xDC3042: [0x672, 3],
0xDC3043: [0x672, 5],
0xDC3044: [0x660, 1], # Ripsaw Rage
0xDC3045: [0x660, 2],
0xDC3046: [0x660, 3],
0xDC3047: [0x660, 5],
0xDC3048: [0x66E, 1], # Blazing Bazukas
0xDC3049: [0x66E, 2],
0xDC304A: [0x66E, 3],
0xDC304B: [0x66E, 5],
0xDC304C: [0x670, 1], # Low-G Labyrinth
0xDC304D: [0x670, 2],
0xDC304E: [0x670, 3],
0xDC304F: [0x670, 5],
0xDC3050: [0x673, 1], # Krevice Kreepers
0xDC3051: [0x673, 2],
0xDC3052: [0x673, 3],
0xDC3053: [0x673, 5],
0xDC3054: [0x65F, 1], # Tearaway Toboggan
0xDC3055: [0x65F, 2],
0xDC3056: [0x65F, 3],
0xDC3057: [0x65F, 5],
0xDC3058: [0x66C, 1], # Barrel Drop Bounce
0xDC3059: [0x66C, 2],
0xDC305A: [0x66C, 3],
0xDC305B: [0x66C, 5],
0xDC305C: [0x66F, 1], # Krack-Shot Kroc
0xDC305D: [0x66F, 2],
0xDC305E: [0x66F, 3],
0xDC305F: [0x66F, 5],
0xDC3060: [0x65E, 1], # Lemguin Lunge
0xDC3061: [0x65E, 2],
0xDC3062: [0x65E, 3],
0xDC3063: [0x65E, 5],
0xDC3064: [0x676, 1], # Buzzer Barrage
0xDC3065: [0x676, 2],
0xDC3066: [0x676, 3],
0xDC3067: [0x676, 5],
0xDC3068: [0x674, 1], # Kong-Fused Cliffs
0xDC3069: [0x674, 2],
0xDC306A: [0x674, 3],
0xDC306B: [0x674, 5],
0xDC306C: [0x669, 1], # Floodlit Fish
0xDC306D: [0x669, 2],
0xDC306E: [0x669, 3],
0xDC306F: [0x669, 5],
0xDC3070: [0x677, 1], # Pothole Panic
0xDC3071: [0x677, 2],
0xDC3072: [0x677, 3],
0xDC3073: [0x677, 5],
0xDC3074: [0x675, 1], # Ropey Rumpus
0xDC3075: [0x675, 2],
0xDC3076: [0x675, 3],
0xDC3077: [0x675, 5],
0xDC3078: [0x67A, 1], # Konveyor Rope Klash
0xDC3079: [0x67A, 2],
0xDC307A: [0x67A, 3],
0xDC307B: [0x67A, 5],
0xDC307C: [0x678, 1], # Creepy Caverns
0xDC307D: [0x678, 2],
0xDC307E: [0x678, 3],
0xDC307F: [0x678, 5],
0xDC3080: [0x665, 1], # Lightning Lookout
0xDC3081: [0x665, 2],
0xDC3082: [0x665, 3],
0xDC3083: [0x665, 5],
0xDC3084: [0x679, 1], # Koindozer Klamber
0xDC3085: [0x679, 2],
0xDC3086: [0x679, 3],
0xDC3087: [0x679, 5],
0xDC3088: [0x671, 1], # Poisonous Pipeline
0xDC3089: [0x671, 2],
0xDC308A: [0x671, 3],
0xDC308B: [0x671, 5],
0xDC308C: [0x67B, 1], # Stampede Sprint
0xDC308D: [0x67B, 2],
0xDC308E: [0x67B, 3],
0xDC308F: [0x67B, 4],
0xDC3090: [0x67B, 5],
0xDC3091: [0x67C, 1], # Criss Kross Cliffs
0xDC3092: [0x67C, 2],
0xDC3093: [0x67C, 3],
0xDC3094: [0x67C, 5],
0xDC3095: [0x67D, 1], # Tyrant Twin Tussle
0xDC3096: [0x67D, 2],
0xDC3097: [0x67D, 3],
0xDC3098: [0x67D, 4],
0xDC3099: [0x67D, 5],
0xDC309A: [0x663, 1], # Swoopy Salvo
0xDC309B: [0x663, 2],
0xDC309C: [0x663, 3],
0xDC309D: [0x663, 4],
0xDC309E: [0x663, 5],
0xDC309F: [0x67E, 1], # Rocket Rush
0xDC30A0: [0x67E, 5],
0xDC30A1: [0x64F, 1], # Bosses
0xDC30A2: [0x650, 1],
0xDC30A3: [0x651, 1],
0xDC30A4: [0x652, 1],
0xDC30A5: [0x653, 1],
0xDC30A6: [0x654, 1],
0xDC30A7: [0x655, 1],
0xDC30A8: [0x656, 1],
0xDC30A9: [0x647, 1], # Banana Bird Caves
0xDC30AA: [0x645, 1],
0xDC30AB: [0x644, 1],
0xDC30AC: [0x642, 1],
0xDC30AD: [0x643, 1],
0xDC30AE: [0x646, 1],
0xDC30AF: [0x648, 1],
0xDC30B0: [0x649, 1],
0xDC30B1: [0x64A, 1],
#0xDC30B2: [0x64B, 1], # Disabled until Trade Sequence
0xDC30B3: [0x64C, 1],
#0xDC30B4: [0x64D, 1], # Disabled until Trade Sequence
0xDC30B5: [0x64E, 1],
0xDC30B6: [0x5FD, 4], # Banana Bird Mother
# DKC3_TODO: Disabled until Trade Sequence
#0xDC30B7: [0x615, 2, True],
#0xDC30B8: [0x615, 3, True],
#0xDC30B9: [0x619, 2],
##0xDC30BA:
#0xDC30BB: [0x61B, 3],
#0xDC30BC: [0x61D, 2],
#0xDC30BD: [0x621, 4],
#0xDC30BE: [0x625, 4, True],
}
item_rom_data = {
0xDC3001: [0x5D5], # 1-Up Balloon
0xDC3002: [0x5C9], # Bear Coin
0xDC3003: [0x5CB], # Bonus Coin
0xDC3004: [0x5CF], # DK Coin
0xDC3005: [0x5CD], # Banana Bird
0xDC3006: [0x5D1, 0x603], # Cog
}
music_rom_data = [
0x3D06B1,
0x3D0753,
0x3D071D,
0x3D07FA,
0x3D07C4,
0x3D08FE,
0x3D096C,
0x3D078E,
0x3D08CD,
0x3D09DD,
0x3D0A0E,
0x3D0AB3,
0x3D06E7,
0x3D0AE4,
0x3D0A45,
0x3D0B46,
0x3D0C40,
0x3D0897,
0x3D0B77,
0x3D0BD9,
0x3D0C71,
0x3D0866,
0x3D0B15,
0x3D0BA8,
0x3D0830,
0x3D0D04,
0x3D0CA2,
0x3D0A7C,
0x3D0D35,
0x3D0CD3,
0x3D0DC8,
0x3D0D66,
0x3D09AC,
0x3D0D97,
0x3D0C0F,
0x3D0DF9,
0x3D0E31,
0x3D0E62,
0x3D0934,
0x3D0E9A,
]
level_music_ids = [
0x06,
0x07,
0x08,
0x0A,
0x0B,
0x0E,
0x0F,
0x10,
0x17,
0x19,
0x1C,
0x1D,
0x1E,
0x21,
]
class LocalRom(object):
def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None):
self.name = name
self.hash = hash
self.orig_buffer = None
with open(file, 'rb') as stream:
self.buffer = read_rom(stream)
#if patch:
# self.patch_rom()
# self.orig_buffer = self.buffer.copy()
#if vanillaRom:
# with open(vanillaRom, 'rb') as vanillaStream:
# self.orig_buffer = read_rom(vanillaStream)
def read_bit(self, address: int, bit_number: int) -> bool:
bitflag = (1 << bit_number)
return ((self.buffer[address] & bitflag) != 0)
def read_byte(self, address: int) -> int:
return self.buffer[address]
def read_bytes(self, startaddress: int, length: int) -> bytes:
return self.buffer[startaddress:startaddress + length]
def write_byte(self, address: int, value: int):
self.buffer[address] = value
def write_bytes(self, startaddress: int, values):
self.buffer[startaddress:startaddress + len(values)] = values
def write_to_file(self, file):
with open(file, 'wb') as outfile:
outfile.write(self.buffer)
def read_from_file(self, file):
with open(file, 'rb') as stream:
self.buffer = bytearray(stream.read())
def patch_rom(world, rom, player, active_level_list):
local_random = world.slot_seeds[player]
# Boomer Costs
bonus_coin_cost = world.krematoa_bonus_coin_cost[player]
inverted_bonus_coin_cost = 0x100 - bonus_coin_cost
rom.write_byte(0x3498B9, inverted_bonus_coin_cost)
rom.write_byte(0x3498BA, inverted_bonus_coin_cost)
rom.write_byte(0x3498BB, inverted_bonus_coin_cost)
rom.write_byte(0x3498BC, inverted_bonus_coin_cost)
rom.write_byte(0x3498BD, inverted_bonus_coin_cost)
rom.write_byte(0x349857, bonus_coin_cost)
rom.write_byte(0x349862, bonus_coin_cost)
# Gyrocopter Costs
dk_coin_cost = world.dk_coins_for_gyrocopter[player]
rom.write_byte(0x3484A6, dk_coin_cost)
rom.write_byte(0x3484D5, dk_coin_cost)
rom.write_byte(0x3484D7, 0x90)
rom.write_byte(0x3484DC, 0xEA)
rom.write_byte(0x3484DD, 0xEA)
rom.write_byte(0x3484DE, 0xEA)
rom.write_byte(0x348528, 0x80) # Prevent Single-Ski Lock
# Make Swanky free
rom.write_byte(0x348C48, 0x00)
# Banana Bird Costs
if world.goal[player] == "banana_bird_hunt":
banana_bird_cost = math.floor(world.number_of_banana_birds[player] * world.percentage_of_banana_birds[player] / 100.0)
rom.write_byte(0x34AB85, banana_bird_cost)
rom.write_byte(0x329FD8, banana_bird_cost)
rom.write_byte(0x32A025, banana_bird_cost)
rom.write_byte(0x329FDA, 0xB0)
else:
# rom.write_byte(0x34AB84, 0x20) # These cause hangs at Wrinkly's
# rom.write_byte(0x329FD8, 0x20)
# rom.write_byte(0x32A025, 0x20)
rom.write_byte(0x329FDA, 0xB0)
# Baffle Mirror Fix
rom.write_byte(0x9133, 0x08)
rom.write_byte(0x9135, 0x0C)
rom.write_byte(0x9136, 0x2B)
rom.write_byte(0x9137, 0x06)
# Palette Swap
rom.write_byte(0x3B96A5, 0xD0)
if world.kong_palette_swap[player] == "default":
rom.write_byte(0x3B96A9, 0x00)
rom.write_byte(0x3B96A8, 0x00)
elif world.kong_palette_swap[player] == "purple":
rom.write_byte(0x3B96A9, 0x00)
rom.write_byte(0x3B96A8, 0x3C)
elif world.kong_palette_swap[player] == "spooky":
rom.write_byte(0x3B96A9, 0x00)
rom.write_byte(0x3B96A8, 0xA0)
elif world.kong_palette_swap[player] == "dark":
rom.write_byte(0x3B96A9, 0x05)
rom.write_byte(0x3B96A8, 0xA0)
elif world.kong_palette_swap[player] == "chocolate":
rom.write_byte(0x3B96A9, 0x1D)
rom.write_byte(0x3B96A8, 0xA0)
elif world.kong_palette_swap[player] == "shadow":
rom.write_byte(0x3B96A9, 0x45)
rom.write_byte(0x3B96A8, 0xA0)
elif world.kong_palette_swap[player] == "red_gold":
rom.write_byte(0x3B96A9, 0x5D)
rom.write_byte(0x3B96A8, 0xA0)
elif world.kong_palette_swap[player] == "gbc":
rom.write_byte(0x3B96A9, 0x20)
rom.write_byte(0x3B96A8, 0x3C)
elif world.kong_palette_swap[player] == "halloween":
rom.write_byte(0x3B96A9, 0x70)
rom.write_byte(0x3B96A8, 0x3C)
if world.music_shuffle[player]:
for address in music_rom_data:
rand_song = local_random.choice(level_music_ids)
rom.write_byte(address, rand_song)
# Starting Lives
rom.write_byte(0x9130, world.starting_life_count[player].value)
rom.write_byte(0x913B, world.starting_life_count[player].value)
# Handle Level Shuffle Here
if world.level_shuffle[player]:
for i in range(len(active_level_list)):
rom.write_byte(level_dict[level_list[i]].nameIDAddress, level_dict[active_level_list[i]].nameID)
rom.write_byte(level_dict[level_list[i]].levelIDAddress, level_dict[active_level_list[i]].levelID)
# First levels of each world
rom.write_byte(0x34BC3E, (0x32 + level_dict[active_level_list[0]].levelID))
rom.write_byte(0x34BC47, (0x32 + level_dict[active_level_list[5]].levelID))
rom.write_byte(0x34BC4A, (0x32 + level_dict[active_level_list[10]].levelID))
rom.write_byte(0x34BC53, (0x32 + level_dict[active_level_list[15]].levelID))
rom.write_byte(0x34BC59, (0x32 + level_dict[active_level_list[20]].levelID))
rom.write_byte(0x34BC5C, (0x32 + level_dict[active_level_list[25]].levelID))
rom.write_byte(0x34BC65, (0x32 + level_dict[active_level_list[30]].levelID))
rom.write_byte(0x34BC6E, (0x32 + level_dict[active_level_list[35]].levelID))
# Cotton-Top Cove Boss Unlock
rom.write_byte(0x34C02A, (0x32 + level_dict[active_level_list[14]].levelID))
# Kong-Fused Cliffs Unlock
rom.write_byte(0x34C213, (0x32 + level_dict[active_level_list[25]].levelID))
rom.write_byte(0x34C21B, (0x32 + level_dict[active_level_list[26]].levelID))
if world.goal[player] == "knautilus":
# Swap Kastle KAOS and Knautilus
rom.write_byte(0x34D4E1, 0xC2)
rom.write_byte(0x34D4E2, 0x24)
rom.write_byte(0x34D551, 0xBA)
rom.write_byte(0x34D552, 0x23)
rom.write_byte(0x32F339, 0x55)
from Main import __version__
rom.name = bytearray(f'D3{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x7FC0, rom.name)
# DKC3_TODO: This is a hack, reconsider
# Don't grant (DK, Bonus, Bear) Coins
rom.write_byte(0x3BD454, 0xEA)
rom.write_byte(0x3BD455, 0xEA)
# Don't grant Cogs
rom.write_byte(0x3BD574, 0xEA)
rom.write_byte(0x3BD575, 0xEA)
rom.write_byte(0x3BD576, 0xEA)
# Don't grant Banana Birds at their caves
rom.write_byte(0x32DD62, 0xEA)
rom.write_byte(0x32DD63, 0xEA)
rom.write_byte(0x32DD64, 0xEA)
# Don't grant Patch and Skis from their bosses
rom.write_byte(0x3F3762, 0x00)
rom.write_byte(0x3F377B, 0x00)
rom.write_byte(0x3F3797, 0x00)
# Always allow Start+Select
rom.write_byte(0x8BAB, 0x01)
# Handle Alt Palettes in Krematoa
rom.write_byte(0x3B97E9, 0x80)
rom.write_byte(0x3B97EA, 0xEA)
class DKC3DeltaPatch(APDeltaPatch):
hash = USHASH
game = "Donkey Kong Country 3"
patch_file_ending = ".apdkc3"
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_bytes()
def get_base_rom_bytes(file_name: str = "") -> bytes:
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
if not base_rom_bytes:
file_name = get_base_rom_path(file_name)
base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if USHASH != basemd5.hexdigest():
raise Exception('Supplied Base Rom does not match known MD5 for US(1.0) release. '
'Get the correct game and version, then dump it')
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
return base_rom_bytes
def get_base_rom_path(file_name: str = "") -> str:
options = Utils.get_options()
if not file_name:
file_name = options["dkc3_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.local_path(file_name)
return file_name

32
worlds/dkc3/Rules.py Normal file
View File

@@ -0,0 +1,32 @@
import math
from BaseClasses import MultiWorld
from .Names import LocationName, ItemName
from ..AutoWorld import LogicMixin
from ..generic.Rules import add_rule, set_rule
def set_rules(world: MultiWorld, player: int):
if False:#world.include_trade_sequence[player]:
add_rule(world.get_location(LocationName.barnacles_island, player),
lambda state: state.has(ItemName.shell, player))
add_rule(world.get_location(LocationName.blues_beach_hut, player),
lambda state: state.has(ItemName.present, player))
add_rule(world.get_location(LocationName.brambles_bungalow, player),
lambda state: state.has(ItemName.flower, player))
add_rule(world.get_location(LocationName.barters_swap_shop, player),
lambda state: state.has(ItemName.mirror, player))
if world.goal[player] != "knautilus":
required_banana_birds = math.floor(
world.number_of_banana_birds[player].value * (world.percentage_of_banana_birds[player].value / 100.0))
add_rule(world.get_location(LocationName.banana_bird_mother, player),
lambda state: state.has(ItemName.banana_bird, player, required_banana_birds))
world.completion_condition[player] = lambda state: state.has(ItemName.victory, player)

208
worlds/dkc3/__init__.py Normal file
View File

@@ -0,0 +1,208 @@
import os
import typing
import math
import threading
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
from .Items import DKC3Item, ItemData, item_table, inventory_table
from .Locations import DKC3Location, all_locations, setup_locations
from .Options import dkc3_options
from .Regions import create_regions, connect_regions
from .Levels import level_list
from .Rules import set_rules
from .Names import ItemName, LocationName
from ..AutoWorld import WebWorld, World
from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch
import Patch
class DKC3Web(WebWorld):
theme = "jungle"
setup_en = Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Donkey Kong Country 3 randomizer connected to an Archipelago Multiworld.",
"English",
"setup_en.md",
"setup/en",
["PoryGone"]
)
tutorials = [setup_en]
class DKC3World(World):
"""
Donkey Kong Country 3 is an action platforming game.
Play as Dixie Kong and her baby cousin Kiddy as they try to solve the
mystery of why Donkey Kong and Diddy disappeared while on vacation.
"""
game: str = "Donkey Kong Country 3"
options = dkc3_options
topology_present = False
data_version = 1
#hint_blacklist = {LocationName.rocket_rush_flag}
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = all_locations
active_level_list: typing.List[str]
web = DKC3Web()
def __init__(self, world: MultiWorld, player: int):
self.rom_name_available_event = threading.Event()
super().__init__(world, player)
@classmethod
def stage_assert_generate(cls, world):
rom_file = get_base_rom_path()
if not os.path.exists(rom_file):
raise FileNotFoundError(rom_file)
def _get_slot_data(self):
return {
#"death_link": self.world.death_link[self.player].value,
"active_levels": self.active_level_list,
}
def _create_items(self, name: str):
data = item_table[name]
return [self.create_item(name)] * data.quantity
def fill_slot_data(self) -> dict:
slot_data = self._get_slot_data()
for option_name in dkc3_options:
option = getattr(self.world, option_name)[self.player]
slot_data[option_name] = option.value
return slot_data
def generate_basic(self):
self.topology_present = self.world.level_shuffle[self.player].value
itempool: typing.List[DKC3Item] = []
# Levels
total_required_locations = 159
number_of_banana_birds = 0
# Rocket Rush Cog
total_required_locations -= 1
number_of_cogs = 4
self.world.get_location(LocationName.rocket_rush_flag, self.player).place_locked_item(self.create_item(ItemName.krematoa_cog))
number_of_bosses = 8
if self.world.goal[self.player] == "knautilus":
self.world.get_location(LocationName.kastle_kaos, self.player).place_locked_item(self.create_item(ItemName.victory))
number_of_bosses = 7
else:
self.world.get_location(LocationName.banana_bird_mother, self.player).place_locked_item(self.create_item(ItemName.victory))
number_of_banana_birds = self.world.number_of_banana_birds[self.player]
# Bosses
total_required_locations += number_of_bosses
# Secret Caves
total_required_locations += 13
## Brothers Bear
if False:#self.world.include_trade_sequence[self.player]:
total_required_locations += 10
number_of_bonus_coins = (self.world.krematoa_bonus_coin_cost[self.player] * 5)
number_of_bonus_coins += math.ceil((85 - number_of_bonus_coins) * self.world.percentage_of_extra_bonus_coins[self.player] / 100)
itempool += [self.create_item(ItemName.bonus_coin)] * number_of_bonus_coins
itempool += [self.create_item(ItemName.dk_coin)] * 41
itempool += [self.create_item(ItemName.banana_bird)] * number_of_banana_birds
itempool += [self.create_item(ItemName.krematoa_cog)] * number_of_cogs
itempool += [self.create_item(ItemName.progressive_boat)] * 3
total_junk_count = total_required_locations - len(itempool)
itempool += [self.create_item(ItemName.bear_coin)] * total_junk_count
self.active_level_list = level_list.copy()
if self.world.level_shuffle[self.player]:
self.world.random.shuffle(self.active_level_list)
connect_regions(self.world, self.player, self.active_level_list)
self.world.itempool += itempool
def generate_output(self, output_directory: str):
try:
world = self.world
player = self.player
rom = LocalRom(get_base_rom_path())
patch_rom(self.world, rom, self.player, self.active_level_list)
self.active_level_list.append(LocationName.rocket_rush_region)
outfilepname = f'_P{player}'
outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \
if world.player_name[player] != 'Player%d' % player else ''
rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc')
rom.write_to_file(rompath)
self.rom_name = rom.name
patch = DKC3DeltaPatch(os.path.splitext(rompath)[0]+DKC3DeltaPatch.patch_file_ending, player=player,
player_name=world.player_name[player], patched_path=rompath)
patch.write()
except:
raise
finally:
if os.path.exists(rompath):
os.unlink(rompath)
self.rom_name_available_event.set() # make sure threading continues and errors are collected
def modify_multidata(self, multidata: dict):
import base64
# wait for self.rom_name to be available.
self.rom_name_available_event.wait()
rom_name = getattr(self, "rom_name", None)
# we skip in case of error, so that the original error in the output thread is the one that gets raised
if rom_name:
new_name = base64.b64encode(bytes(self.rom_name)).decode()
multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]]
if self.topology_present:
world_names = [
LocationName.lake_orangatanga_region,
LocationName.kremwood_forest_region,
LocationName.cotton_top_cove_region,
LocationName.mekanos_region,
LocationName.k3_region,
LocationName.razor_ridge_region,
LocationName.kaos_kore_region,
LocationName.krematoa_region,
]
er_hint_data = {}
for world_index in range(len(world_names)):
for level_index in range(5):
level_region = self.world.get_region(self.active_level_list[world_index * 5 + level_index], self.player)
for location in level_region.locations:
er_hint_data[location.address] = world_names[world_index]
multidata['er_hint_data'][self.player] = er_hint_data
def create_regions(self):
location_table = setup_locations(self.world, self.player)
create_regions(self.world, self.player, location_table)
def create_item(self, name: str, force_non_progression=False) -> Item:
data = item_table[name]
if force_non_progression:
classification = ItemClassification.filler
elif data.progression:
classification = ItemClassification.progression
else:
classification = ItemClassification.filler
created_item = DKC3Item(name, classification, data.code, self.player)
return created_item
def set_rules(self):
set_rules(self.world, self.player)

View File

@@ -0,0 +1,35 @@
# Donkey Kong Country 3
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
## What does randomization do to this game?
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is
always able to be completed, but because of the item shuffle the player may need to access certain areas before they
would in the vanilla game.
## What is the goal of Donkey Kong Country 3 when randomized?
There are two goals which can be chosen:
- `Knautilus`: Collect Bonus Coins and Krematoa Cogs to reach K. Rool's submarine in Krematoa
- `Banana Bird Hunt`: Collect Banana Birds to free the Banana Bird Mother
## What items and locations get shuffled?
All Bonus Coins, DK Coins, and Banana Birds (if on a `Banana Bird Hunt` goal) are randomized. Additionally, level clears award a location check.
The Patch and two Skis for upgrading the boat are included. Bear Coins are provided if additional items are needed for the item pool.
Four of the Five Krematoa Cogs are randomized, but the final one is always in its vanilla location at the Flag of Rocket Rush in Krematoa
## Which items can be in another player's world?
Any shuffled item can be in other players' worlds.
## What does another world's item look like in Donkey Kong Country 3
Items pickups all retain their original appearance. You won't know if an item belongs to another player until you collect.
## When the player receives an item, what happens?
Currently, the items are silently added to the player's inventory, which can be seen when saving the game.

View File

@@ -0,0 +1,161 @@
# Donkey Kong Country 3 Randomizer Setup Guide
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Donkey Kong Country 3 Patch Setup`
- Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of connecting to SNI such as:
- snes9x Multitroid
from: [snes9x Multitroid Download](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
- BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html)
- RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or,
- An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other
compatible hardware
- Your legally obtained Donkey Kong Country 3 ROM file, probably named `Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc`
## Installation Procedures
### Windows Setup
1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
or you are on an older version, you may run the installer again to install the SNI Client.
2. During setup, you will be asked to locate your base ROM file. This is your Donkey Kong Country 3 ROM file.
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
2. Right-click on a ROM file and select **Open with...**
3. Check the box next to **Always use this app to open .sfc files**
4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**
5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you
extracted in step one.
## Create a Config (.yaml) File
### What is a config file and why do I need one?
See the guide on setting up a basic YAML at the Archipelago setup
guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Where do I get a config file?
The Player Settings page on the website allows you to configure your personal settings and export a config file from
them. Player settings page: [Donkey Kong Country 3 Player Settings Page](/games/Donkey%20Kong%20Country%203/player-settings)
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
validator page: [YAML Validation page](/mysterycheck)
## Generating a Single-Player Game
1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button.
- Player Settings page: [Donkey Kong Country 3 Player Settings Page](/games/Donkey%20Kong%20Country%203/player-settings)
2. You will be presented with a "Seed Info" page.
3. Click the "Create New Room" link.
4. You will be presented with a server page, from which you can download your patch file.
5. Double-click on your patch file, and the Donkey Kong Country 3 Client will launch automatically, create your ROM from the
patch file, and open your emulator for you.
6. Since this is a single-player game, you will no longer need the client, so feel free to close it.
## Joining a MultiWorld Game
### Obtain your patch file and create your ROM
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done,
the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch
files. Your patch file should have a `.apsm` extension.
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the
client, and will also create your ROM in the same place as your patch file.
### Connect to the client
#### With an emulator
When the client launched automatically, SNI should have also automatically launched in the background. If this is its
first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
##### snes9x Multitroid
1. Load your ROM file if it hasn't already been loaded.
2. Click on the File menu and hover on **Lua Scripting**
3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...**
5. Select the connector lua file included with your client
- Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
emulator is 64-bit or 32-bit.
6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of
the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install.
##### BizHawk
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following these
menu options:
`Config --> Cores --> SNES --> BSNES`
Once you have changed the loaded core, you must restart BizHawk.
2. Load your ROM file if it hasn't already been loaded.
3. Click on the Tools menu and click on **Lua Console**
4. Click the button to open a new Lua script.
5. Select the `Connector.lua` file included with your client
- Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
##### RetroArch 1.10.3 or newer
You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.3.
1. Enter the RetroArch main menu screen.
2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON.
3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default
Network Command Port at 55355.
![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png)
4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury
Performance)".
When loading a ROM, be sure to select a **bsnes-mercury** core. These are the only cores that allow external tools to
read ROM data.
#### With hardware
This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do
this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES
releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases)
Other hardware may find helpful information on the usb2snes platforms
page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms)
1. Close your emulator, which may have auto-launched.
2. Power on your device and load the ROM.
### Connect to the Archipelago Server
The patch file which launched your client should have automatically connected you to the AP Server. There are a few
reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the
client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it
into the "Server" input field then press enter.
The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected".
### Play the game
When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on
successfully joining a multiworld game!
## Hosting a MultiWorld game
The recommended way to host a game is to use our hosting service. The process is relatively simple:
1. Collect config files from your players.
2. Create a zip file containing your players' config files.
3. Upload that zip file to the Generate page above.
- Generate page: [WebHost Seed Generation Page](/generate)
4. Wait a moment while the seed is generated.
5. When the seed is generated, you will be redirected to a "Seed Info" page.
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so
they may download their patch files from there.
7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all
players in the game. Any observers may also be given the link to this page.
8. Once all players have joined, you may begin playing.

View File

@@ -501,6 +501,7 @@ def get_science_pack_pools() -> Dict[str, Set[str]]:
item_stack_sizes: Dict[str, int] = items_future.result()
non_stacking_items: Set[str] = {item for item, stack in item_stack_sizes.items() if stack == 1}
stacking_items: Set[str] = set(item_stack_sizes) - non_stacking_items
valid_ingredients: Set[str] = stacking_items | fluids
# cleanup async helpers
pool.shutdown()

View File

@@ -8,7 +8,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \
fluids, stacking_items
fluids, stacking_items, valid_ingredients
from .Shapes import get_shapes
from .Mod import generate_mod
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal
@@ -221,7 +221,7 @@ class Factorio(World):
# Return the liquid to the pool and get a new ingredient.
pool.append(new_ingredient)
new_ingredient = pool.pop(0)
liquids_used += 1
liquids_used += 1 if new_ingredient in fluids else 0
new_ingredients[new_ingredient] = 1
return Recipe(original.name, self.get_category(original.category, liquids_used), new_ingredients,
original.products, original.energy)
@@ -231,7 +231,7 @@ class Factorio(World):
"""Generate a recipe from pool with time and cost similar to original * factor"""
new_ingredients = {}
# have to first sort for determinism, while filtering out non-stacking items
pool: typing.List[str] = sorted(pool & stacking_items)
pool: typing.List[str] = sorted(pool & valid_ingredients)
# then sort with random data to shuffle
self.world.random.shuffle(pool)
target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor)
@@ -329,10 +329,8 @@ class Factorio(World):
def set_custom_recipes(self):
original_rocket_part = recipes["rocket-part"]
science_pack_pools = get_science_pack_pools()
valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()])
valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()] & valid_ingredients)
self.world.random.shuffle(valid_pool)
while any([valid_pool[x] in fluids for x in range(3)]):
self.world.random.shuffle(valid_pool)
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
{valid_pool[x]: 10 for x in range(3)},
original_rocket_part.products,

View File

@@ -1,6 +1,48 @@
{% from "macros.lua" import dict_to_recipe %}
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
require('lib')
data.raw["rocket-silo"]["rocket-silo"].fluid_boxes = {
{
production_type = "input",
pipe_picture = assembler2pipepictures(),
pipe_covers = pipecoverspictures(),
base_area = 10,
base_level = -1,
pipe_connections = {
{ type = "input", position = { 0, 5 } },
{ type = "input", position = { 0, -5 } },
{ type = "input", position = { 5, 0 } },
{ type = "input", position = { -5, 0 } }
}
},
{
production_type = "input",
pipe_picture = assembler2pipepictures(),
pipe_covers = pipecoverspictures(),
base_area = 10,
base_level = -1,
pipe_connections = {
{ type = "input", position = { -3, 5 } },
{ type = "input", position = { -3, -5 } },
{ type = "input", position = { 5, -3 } },
{ type = "input", position = { -5, -3 } }
}
},
{
production_type = "input",
pipe_picture = assembler2pipepictures(),
pipe_covers = pipecoverspictures(),
base_area = 10,
base_level = -1,
pipe_connections = {
{ type = "input", position = { 3, 5 } },
{ type = "input", position = { 3, -5 } },
{ type = "input", position = { 5, 3 } },
{ type = "input", position = { -5, 3 } }
}
},
off_when_no_fluid_recipe = true
}
{%- for recipe_name, recipe in custom_recipes.items() %}
data.raw["recipe"]["{{recipe_name}}"].category = "{{recipe.category}}"

View File

@@ -1 +1 @@
factorio-rcon-py>=1.2.1
factorio-rcon-py>=2.0.1

View File

@@ -1,5 +1,5 @@
from typing import Dict
from BaseClasses import Item, Location, MultiWorld, Tutorial
from BaseClasses import Item, Location, MultiWorld, Tutorial, ItemClassification
from .Items import ItemData, FF1Items, FF1_STARTER_ITEMS, FF1_PROGRESSION_LIST, FF1_BRIDGE
from .Locations import EventId, FF1Locations, generate_rule, CHAOS_TERMINATED_EVENT
from .Options import ff1_options
@@ -55,7 +55,7 @@ class FF1World(World):
rules = get_options(self.world, 'rules', self.player)
menu_region = self.ff1_locations.create_menu_region(self.player, locations, rules)
terminated_event = Location(self.player, CHAOS_TERMINATED_EVENT, EventId, menu_region)
terminated_item = Item(CHAOS_TERMINATED_EVENT, True, EventId, self.player)
terminated_item = Item(CHAOS_TERMINATED_EVENT, ItemClassification.progression, EventId, self.player)
terminated_event.place_locked_item(terminated_item)
items = get_options(self.world, 'items', self.player)
@@ -114,5 +114,6 @@ class FF1World(World):
def get_filler_item_name(self) -> str:
return self.world.random.choice(["Heal", "Pure", "Soft", "Tent", "Cabin", "House"])
def get_options(world: MultiWorld, name: str, player: int):
return getattr(world, name, None)[player].value

View File

@@ -15,6 +15,8 @@ class GenericWeb(WebWorld):
commands = Tutorial('Archipelago Server and Client Commands',
'A guide detailing the commands available to the user when participating in an Archipelago session.',
'English', 'commands_en.md', 'commands/en', ['jat2980', 'Ijwu'])
mac = Tutorial('Archipelago Setup Guide for Mac', 'A guide detailing how to run Archipelago clients on macOS.',
'English', 'mac_en.md','mac/en', ['Bicoloursnake'])
plando = Tutorial('Archipelago Plando Guide', 'A guide to understanding and using plando for your game.',
'English', 'plando_en.md', 'plando/en', ['alwaysintreble', 'Alchav'])
setup = Tutorial('Multiworld Setup Tutorial',
@@ -25,7 +27,7 @@ class GenericWeb(WebWorld):
using_website = Tutorial('Archipelago Website User Guide',
'A guide to using the Archipelago website to generate multiworlds or host pre-generated multiworlds.',
'English', 'using_website_en.md', 'using_website/en', ['alwaysintreble'])
tutorials = [setup, using_website, commands, advanced_settings, triggers, plando]
tutorials = [setup, using_website, mac, commands, advanced_settings, triggers, plando]
class GenericWorld(World):

View File

@@ -52,7 +52,7 @@ For `nested_option_two`, `option_two_setting_one` will be rolled 14 times and `o
times against each other. This means `option_two_setting_two` will be more likely to occur, but it isn't guaranteed,
adding more randomness and "mystery" to your settings. Every configurable setting supports weights.
### Root Options
## Root Options
Currently, there are only a few options that are root options. Everything else should be nested within one of these root
options or in some cases nested within other nested options. The only options that should exist in root
@@ -95,14 +95,14 @@ games you want settings for.
more triggers in the triggers guide. Triggers
guide: [Archipelago Triggers Guide](/tutorial/Archipelago/triggers/en)
### Game Options
## Game Options
One of your root settings will be the name of the game you would like to populate with settings. Since it is possible to
give a weight to any option it is possible to have one file that can generate a seed for you where you don't know which
game you'll play. For these cases you'll want to fill the game options for every game that can be rolled by these
settings. If a game can be rolled it **must** have a settings section even if it is empty.
#### Universal Game Options
### Universal Game Options
Some options in Archipelago can be used by every game but must still be placed within the relevant game's section.
@@ -174,6 +174,8 @@ A Link to the Past:
- Moon Pearl
start_location_hints:
- Spike Cave
priority_locations:
- Link's House
exclude_locations:
- Cave 45
item_links:
@@ -207,9 +209,10 @@ Timespinner:
* `description` gives us a general overview so if we pull up this file later we can understand the intent.
* `name` is `Example Player` and this will be used in the server console when sending and receiving items.
* `game` has an equal chance of being either `A Link to the Past` or `Timespinner` with a 10/20 chance for each. The reason for this is becuase each game has a weight of 10 and the toal of all weights is 20.
* `game` has an equal chance of being either `A Link to the Past` or `Timespinner` with a 10/20 chance for each. This is
because each game has a weight of 10 and the total of all weights is 20.
* `requires` is set to required release version 0.3.2 or higher.
* `accesibility` is set to `none` which will set this seed to beatable only meaning some locations and items may be
* `accessibility` is set to `none` which will set this seed to beatable only, so some locations and items may be
completely inaccessible but the seed will still be completable.
* `progression_balancing` is set on, giving it the default value, meaning we will likely receive important items
earlier increasing the chance of having things to do.
@@ -225,8 +228,8 @@ Timespinner:
1 and 7 will be chosen at random, weighted towards a high number.
* `start_inventory` defines an area for us to determine what items we would like to start the seed with. For this
example we have:
* `Pegasus Boots: 1` which gives us 1 copy of the Pegasus Boots
* `Bombs (3)` gives us 2 packs of 3 bombs or 6 total bombs
* `Pegasus Boots: 1` which gives us 1 copy of the Pegasus Boots
* `Bombs (3): 2` gives us 2 packs of 3 bombs or 6 total bombs
* `start_hints` gives us a starting hint for the hammer available at the beginning of the multiworld which we can use
with no cost.
* `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we
@@ -234,22 +237,19 @@ Timespinner:
* `non_local_items` forces the `Moon Pearl` to be placed in someone else's world, meaning we won't be able to find it.
* `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the
multiworld that can be used for no cost.
* `priority_locations` forces a progression item to be placed on the `Link's House` location.
* `exclude_locations` forces a not important item to be placed on the `Cave 45` location.
* `item_links`
* For `A Link to the Past` all players in the `rods` item link group will share their fire and ice rods and the player
items will be replaced with single rupees.
* For `Timespinner` all players in the `TSAll` item link group will share their entire item pool and the`Twin Pyramid
* For `A Link to the Past` all players in the `rods` item link group will share their fire and ice rods and the player
items will be replaced with single rupees.
items will be replaced with single rupees.
* For `Timespinner` all players in the `TSAll` item link group will share their entire item pool and the `Twin Pyramid
Key` and `Timespinner Wheel` will be forced among the worlds of those in the group. The `null` replacement item will, instead
of forcing a specific chosen item, allow the generator to randomly pick a filler item in place of putting in another one of the linked item.
Key` and `Timespinner Wheel` will be forced among the worlds of those in the group. The `null` replacement item will,
instead of forcing a specific chosen item, allow the generator to randomly pick a filler item to replace the player items.
* `triggers` allows us to define a trigger such that if our `smallkey_shuffle` option happens to roll the `any_world`
result it will also ensure that `bigkey_shuffle`, `map_shuffle`, and `compass_shuffle` are also forced to
the `any_world`
result.
result it will also ensure that `bigkey_shuffle`, `map_shuffle`, and `compass_shuffle` are also forced to the
`any_world` result. More information on triggers can be found in the [triggers guide](/tutorial/Archipelago/triggers/en).
### Generating Multiple Worlds
## Generating Multiple Worlds
YAML files can be configured to generate multiple worlds using only one file. This is mostly useful if you are playing an asynchronous multiworld (shortened to async) and are wanting to submit multiple worlds as they can be condensed into one file, removing the need to manage separate files if one chooses to do so.
@@ -257,7 +257,7 @@ As a precautionary measure, before submitting a multi-game yaml like this one in
To configure your file to generate multiple worlds, use 3 dashes `---` on an empty line to separate the ending of one world and the beginning of another world.
#### Example
### Example
```yaml
description: Example of generating multiple worlds. World 1 of 3
@@ -272,7 +272,7 @@ Super Mario 64:
StrictCapRequirements: true
StrictCannonRequirements: true
StarsToFinish: 70
ExtraStars: 30
AmountOfStars: 70
DeathLink: true
BuddyChecks: true
AreaRandomizer: true

View File

@@ -0,0 +1,32 @@
# Guide to Run Archipelago from Source Code on macOS
Archipelago does not have a compiled release on macOS. However, it is possible to run from source code on macOS. This guide expects you to have some experience with running software from the terminal.
## Prerequisite Software
Here is a list of software to install and source code to download.
1. Python 3.8 or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/).
2. Xcode from the [macOS App Store](https://apps.apple.com/us/app/xcode/id497799835).
3. The source code from the [Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases).
4. The asset with darwin in the name from the [SNI Github releases page](https://github.com/alttpo/sni/releases).
5. If you would like to generate Enemized seeds for ALTTP locally (not on the website), you may need the EnemizerCLI from its [Github releases page](https://github.com/Ijwu/Enemizer/releases).
6. An Emulator of your choosing for games that need an emulator. For SNES games, I recommend RetroArch, entirely because it was the easiest for me to setup on macOS. It can be downloaded at the [RetroArch downloads page](https://www.retroarch.com/?page=platforms)
## Extracting the Archipelago Directory
1. Double click on the Archipelago source code zip file to extract the files to an Archipelago directory.
2. Move this Archipelago directory out of your downloads directory.
3. Open terminal and navigate to your Archipelago directory.
## Setting up a Virtual Environment
It is generally recommended that you use a virtual environment to run python based software to avoid contamination that can break some software. If Archipelago is the only piece of software you use that runs from python source code however, it is not necessary to use a virtual environment.
1. Open terminal and navigate to the Archipelago directory.
2. Run the command `python3 -m venv venv` to create a virtual environment. Running this command will create a new directory at the specified path, so make sure that path is clear for a new directory to be created.
3. Run the command `source venv/bin/activate` to activate the virtual environment.
4. If you want to exit the virtual environment, run the command `deactivate`.
## Steps to Run the Clients
1. If your game doesn't have a patch file, run the command `python3 SNIClient.py`, changing the filename with the file of the client you want to run.
2. If your game does have a patch file, move the base rom to the Archipelago directory and run the command `python3 SNIClient.py 'patchfile'` with the filename extension for the patch file (apsm, aplttp, apsmz3, etc.) included and changing the filename with the file of the client you want to run.
3. Your client should now be running and rom created (where applicable).
## Additional Steps for SNES Games
1. If using RetroArch, the instructions to set up your emulator [here in the Link to the Past setup guide](https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/multiworld/en) also work on the macOS version of RetroArch.
2. Double click on the SNI tar.gz download to extract the files to an SNI directory. If it isn't already, rename this directory to SNI to make some steps easier.
3. Move the SNI directory out of the downloads directory, preferably into the Archipelago directory created earlier.
4. If the SNI directory is correctly named and moved into the Archipelago directory, it should auto run with the SNI client. If it doesn't automatically run, open up the SNI directory and run the SNI executable file manually.
5. If using EnemizerCLI, extract that downloaded directory and rename it to EnemizerCLI.
6. Move the EnemizerCLI directory into the Archipelago directory so that Generate.py can take advantage of it.
7. Now that SNI, the client, and the emulator are all running, you should be good to go.

File diff suppressed because one or more lines are too long

View File

@@ -74,7 +74,7 @@ class Absorber(ast.NodeTransformer):
self.truth_values = truth_values
self.truth_values |= {"True", "None", "ANY", "ITEMRANDO"}
self.false_values = false_values
self.false_values |= {"False", "NONE", "RANDOMELEVATORS"}
self.false_values |= {"False", "NONE"}
super(Absorber, self).__init__()
@@ -203,7 +203,58 @@ logic_folder = os.path.join(resources_source, "Logic")
logic_options: typing.Dict[str, str] = hk_loads(os.path.join(data_folder, "logic_settings.json"))
for logic_key, logic_value in logic_options.items():
logic_options[logic_key] = logic_value.split(".", 1)[-1]
del (logic_options["RANDOMELEVATORS"])
vanilla_cost_data: typing.Dict[str, typing.Dict[str, typing.Any]] = hk_loads(os.path.join(data_folder, "costs.json"))
vanilla_location_costs = {
key: {
value["term"]: int(value["amount"])
}
for key, value in vanilla_cost_data.items()
if value["amount"] > 0 and value["term"] == "GEO"
}
salubra_geo_costs_by_charm_count = {
5: 120,
10: 500,
18: 900,
25: 1400,
40: 800
}
# Can't extract this data, so supply it ourselves. Source: the wiki
vanilla_shop_costs = {
('Sly', 'Simple_Key'): [{'GEO': 950}],
('Sly', 'Rancid_Egg'): [{'GEO': 60}],
('Sly', 'Lumafly_Lantern'): [{'GEO': 1800}],
('Sly', 'Gathering_Swarm'): [{'GEO': 300}],
('Sly', 'Stalwart_Shell'): [{'GEO': 200}],
('Sly', 'Mask_Shard'): [
{'GEO': 150},
{'GEO': 500},
],
('Sly', 'Vessel_Fragment'): [{'GEO': 550}],
('Sly_(Key)', 'Heavy_Blow'): [{'GEO': 350}],
('Sly_(Key)', 'Elegant_Key'): [{'GEO': 800}],
('Sly_(Key)', 'Mask_Shard'): [
{'GEO': 800},
{'GEO': 1500},
],
('Sly_(Key)', 'Vessel_Fragment'): [{'GEO': 900}],
('Sly_(Key)', 'Sprintmaster'): [{'GEO': 400}],
('Iselda', 'Wayward_Compass'): [{'GEO': 220}],
('Iselda', 'Quill'): [{'GEO': 120}],
('Salubra', 'Lifeblood_Heart'): [{'GEO': 250}],
('Salubra', 'Longnail'): [{'GEO': 300}],
('Salubra', 'Steady_Body'): [{'GEO': 120}],
('Salubra', 'Shaman_Stone'): [{'GEO': 220}],
('Salubra', 'Quick_Focus'): [{'GEO': 800}],
('Leg_Eater', 'Fragile_Heart'): [{'GEO': 350}],
('Leg_Eater', 'Fragile_Greed'): [{'GEO': 250}],
('Leg_Eater', 'Fragile_Strength'): [{'GEO': 600}],
}
extra_pool_options: typing.List[typing.Dict[str, typing.Any]] = hk_loads(os.path.join(data_folder, "pools.json"))
pool_options: typing.Dict[str, typing.Tuple[typing.List[str], typing.List[str]]] = {}
for option in extra_pool_options:
@@ -213,8 +264,23 @@ for option in extra_pool_options:
for pairing in option["Vanilla"]:
items.append(pairing["item"])
location_name = pairing["location"]
if any(cost_entry["term"] == "CHARMS" for cost_entry in pairing.get("costs", [])):
location_name += "_(Requires_Charms)"
item_costs = pairing.get("costs", [])
if item_costs:
if any(cost_entry["term"] == "CHARMS" for cost_entry in item_costs):
location_name += "_(Requires_Charms)"
#vanilla_shop_costs[pairing["location"], pairing["item"]] = \
cost = {
entry["term"]: int(entry["amount"]) for entry in item_costs
}
# Rando4 doesn't include vanilla geo costs for Salubra charms, so dirty hardcode here.
if 'CHARMS' in cost:
geo = salubra_geo_costs_by_charm_count.get(cost['CHARMS'])
if geo:
cost['GEO'] = geo
key = (pairing["location"], pairing["item"])
vanilla_shop_costs.setdefault(key, []).append(cost)
locations.append(location_name)
if option["Path"]:
# basename carries over from prior entry if no Path given
@@ -229,6 +295,12 @@ for option in extra_pool_options:
pool_options[basename] = items, locations
del extra_pool_options
# reverse all the vanilla shop costs (really, this is just for Salubra).
# When we use these later, we pop off the end of the list so this ensures they are still sorted.
vanilla_shop_costs = {
k: list(reversed(v)) for k, v in vanilla_shop_costs.items()
}
# items
items: typing.Dict[str, typing.Dict] = hk_loads(os.path.join(data_folder, "items.json"))
logic_items: typing.Set[str] = set()
@@ -364,9 +436,15 @@ for event in events:
event_rules.update(connectors_rules)
connectors_rules = {}
# Apply some final fixes
item_effects.update({
'Left_Mothwing_Cloak': {'LEFTDASH': 1},
'Right_Mothwing_Cloak': {'RIGHTDASH': 1},
})
names = sorted({"logic_options", "starts", "pool_options", "locations", "multi_locations", "location_to_region_lookup",
"event_names", "item_effects", "items", "logic_items", "region_names",
"exits", "connectors", "one_ways"})
"exits", "connectors", "one_ways", "vanilla_shop_costs", "vanilla_location_costs"})
warning = "# This module is written by Extractor.py, do not edit manually!.\n\n"
with open(os.path.join(os.path.dirname(__file__), "ExtractedData.py"), "wt") as py:
py.write(warning)
@@ -385,6 +463,6 @@ rules_template = template_env.get_template("RulesTemplate.pyt")
rules = rules_template.render(location_rules=location_rules, one_ways=one_ways, connectors_rules=connectors_rules,
event_rules=event_rules)
with open("Rules.py", "wt") as py:
with open("GeneratedRules.py", "wt") as py:
py.write(warning)
py.write(rules)

1699
worlds/hk/GeneratedRules.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import typing
from .ExtractedData import logic_options, starts, pool_options
from .Rules import cost_terms
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, SpecialRange
from .Charms import vanilla_costs, names as charm_names
@@ -11,19 +12,6 @@ else:
Random = typing.Any
class Disabled(Toggle):
def __init__(self, value: int):
super(Disabled, self).__init__(0)
@classmethod
def from_text(cls, text: str) -> Toggle:
return cls(0)
@classmethod
def from_any(cls, data: typing.Any):
return cls(0)
locations = {"option_" + start: i for i, start in enumerate(starts)}
# This way the dynamic start names are picked up by the MetaClass Choice belongs to
StartLocation = type("StartLocation", (Choice,), {"__module__": __name__, "auto_display_name": False, **locations,
@@ -36,6 +24,8 @@ option_docstrings = {
"randomization.",
"RandomizeSkills": "Allow for Skills, such as Mantis Claw or Shade Soul, to be randomized into the item pool. "
"Also opens their locations for receiving randomized items.",
"RandomizeFocus": "Removes the ability to focus and randomizes it into the item pool.",
"RandomizeSwim": "Removes the ability to swim in water and randomizes it into the item pool.",
"RandomizeCharms": "Allow for Charms to be randomized into the item pool and open their locations for "
"randomization. Includes Charms sold in shops.",
"RandomizeKeys": "Allow for Keys to be randomized into the item pool. Includes those sold in shops.",
@@ -59,6 +49,8 @@ option_docstrings = {
"RandomizeBossEssence": "Randomize boss essence drops, such as those for defeating Warrior Dreams, into the item "
"pool and open their locations for randomization.",
"RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.",
"RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization."
"Mimic Grubs are always placed in your own game.",
"RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see"
" and buy an item that is randomized into that location as well.",
"RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items "
@@ -70,6 +62,7 @@ option_docstrings = {
"RandomizeJournalEntries": "Randomize the Hunter's Journal as well as the findable journal entries into the item "
"pool, and open their locations for randomization. Does not include journal entries "
"gained by killing enemies.",
"RandomizeNail": "Removes the ability to swing the nail left, right and up, and shuffles these into the item pool.",
"RandomizeGeoRocks": "Randomize Geo Rock rewards into the item pool and open their locations for randomization.",
"RandomizeBossGeo": "Randomize boss Geo drops into the item pool and open those locations for randomization.",
"RandomizeSoulTotems": "Randomize Soul Refill items into the item pool and open the Soul Totem locations for"
@@ -110,12 +103,16 @@ default_on = {
"RandomizeRelics"
}
# not supported at this time
disabled = {
"RandomizeFocus",
"RandomizeSwim",
"RandomizeMimics",
"RandomizeNail",
shop_to_option = {
"Seer": "SeerRewardSlots",
"Grubfather": "GrubfatherRewardSlots",
"Sly": "SlyShopSlots",
"Sly_(Key)": "SlyKeyShopSlots",
"Iselda": "IseldaShopSlots",
"Salubra": "SalubraShopSlots",
"Leg_Eater": "LegEaterShopSlots",
"Salubra_(Requires_Charms)": "IseldaShopSlots",
"Egg_Shop": "EggShopSlots",
}
hollow_knight_randomize_options: typing.Dict[str, type(Option)] = {}
@@ -124,9 +121,6 @@ for option_name, option_data in pool_options.items():
extra_data = {"__module__": __name__, "items": option_data[0], "locations": option_data[1]}
if option_name in option_docstrings:
extra_data["__doc__"] = option_docstrings[option_name]
if option_name in disabled:
extra_data["__doc__"] = "Disabled Option. Not implemented."
option = type(option_name, (Disabled,), extra_data)
if option_name in default_on:
option = type(option_name, (DefaultOnToggle,), extra_data)
else:
@@ -142,13 +136,36 @@ for option_name in logic_options.values():
if option_name in option_docstrings:
extra_data["__doc__"] = option_docstrings[option_name]
option = type(option_name, (Toggle,), extra_data)
if option_name in disabled:
extra_data["__doc__"] = "Disabled Option. Not implemented."
option = type(option_name, (Disabled,), extra_data)
globals()[option.__name__] = option
hollow_knight_logic_options[option.__name__] = option
class RandomizeElevatorPass(Toggle):
"""Adds an Elevator Pass item to the item pool, which is then required to use the large elevators connecting
City of Tears to the Forgotten Crossroads and Resting Grounds."""
display_name = "Randomize Elevator Pass"
default = False
class SplitMothwingCloak(Toggle):
"""Splits the Mothwing Cloak into left- and right-only versions of the item. Randomly adds a second left or
right Mothwing cloak item which functions as the upgrade to Shade Cloak."""
display_name = "Split Mothwing Cloak"
default = False
class SplitMantisClaw(Toggle):
"""Splits the Mantis Claw into left- and right-only versions of the item."""
display_name = "Split Mantis Claw"
default = False
class SplitCrystalHeart(Toggle):
"""Splits the Crystal Heart into left- and right-only versions of the item."""
display_name = "Split Crystal Heart"
default = False
class MinimumGrubPrice(Range):
"""The minimum grub price in the range of prices that an item should cost from Grubfather."""
display_name = "Minimum Grub Price"
@@ -178,7 +195,7 @@ class MaximumEssencePrice(MinimumEssencePrice):
class MinimumEggPrice(Range):
"""The minimum rancid egg price in the range of prices that an item should cost from Ijii.
"""The minimum rancid egg price in the range of prices that an item should cost from Jiji.
Only takes effect if the EggSlotShops option is greater than 0."""
display_name = "Minimum Egg Price"
range_start = 1
@@ -187,7 +204,7 @@ class MinimumEggPrice(Range):
class MaximumEggPrice(MinimumEggPrice):
"""The maximum rancid egg price in the range of prices that an item should cost from Ijii.
"""The maximum rancid egg price in the range of prices that an item should cost from Jiji.
Only takes effect if the EggSlotShops option is greater than 0."""
display_name = "Maximum Egg Price"
default = 10
@@ -205,9 +222,26 @@ class MinimumCharmPrice(Range):
class MaximumCharmPrice(MinimumCharmPrice):
"""The maximum charm price in the range of prices that an item should cost for Salubra's shop item which also
carry a charm cost."""
display_name = "Maximum Charm Requirement"
default = 20
class MinimumGeoPrice(Range):
"""The minimum geo price for items in geo shops."""
display_name = "Minimum Geo Price"
range_start = 1
range_end = 200
default = 1
class MaximumGeoPrice(Range):
"""The maximum geo price for items in geo shops."""
display_name = "Maximum Geo Price"
range_start = 1
range_end = 2000
default = 400
class RandomCharmCosts(SpecialRange):
"""Total Notch Cost of all Charms together. Vanilla sums to 90.
This value is distributed among all charms in a random fashion.
@@ -256,13 +290,91 @@ class PlandoCharmCosts(OptionDict):
return charm_costs
class SlyShopSlots(Range):
"""For each extra slot, add a location to the Sly Shop and a filler item to the item pool."""
display_name = "Sly Shop Slots"
default = 8
range_end = 16
class SlyKeyShopSlots(Range):
"""For each extra slot, add a location to the Sly Shop (requiring Shopkeeper's Key) and a filler item to the item pool."""
display_name = "Sly Key Shop Slots"
default = 6
range_end = 16
class IseldaShopSlots(Range):
"""For each extra slot, add a location to the Iselda Shop and a filler item to the item pool."""
display_name = "Iselda Shop Slots"
default = 2
range_end = 16
class SalubraShopSlots(Range):
"""For each extra slot, add a location to the Salubra Shop, and a filler item to the item pool."""
display_name = "Salubra Shop Slots"
default = 5
range_start = 0
range_end = 16
class SalubraCharmShopSlots(Range):
"""For each extra slot, add a location to the Salubra Shop (requiring Charms), and a filler item to the item pool."""
display_name = "Salubra Charm Shop Slots"
default = 5
range_end = 16
class LegEaterShopSlots(Range):
"""For each extra slot, add a location to the Leg Eater Shop and a filler item to the item pool."""
display_name = "Leg Eater Shop Slots"
default = 3
range_end = 16
class GrubfatherRewardSlots(Range):
"""For each extra slot, add a location to the Grubfather and a filler item to the item pool."""
display_name = "Grubfather Reward Slots"
default = 7
range_end = 16
class SeerRewardSlots(Range):
"""For each extra slot, add a location to the Seer and a filler item to the item pool."""
display_name = "Seer Reward Reward Slots"
default = 8
range_end = 16
class EggShopSlots(Range):
"""For each slot, add a location to the Egg Shop and a Geo drop to the item pool."""
"""For each slot, add a location to the Egg Shop and a filler item to the item pool."""
display_name = "Egg Shop Item Slots"
range_end = 16
class ExtraShopSlots(Range):
"""For each extra slot, add a location to a randomly chosen shop a filler item to the item pool.
The Egg Shop will be excluded from this list unless it has at least one item.
Shops are capped at 16 items each.
"""
display_name = "Additional Shop Slots"
default = 0
range_end = 9 * 16 # Number of shops x max slots per shop.
class Goal(Choice):
"""The goal required of you in order to complete your run in Archipelago."""
display_name = "Goal"
@@ -288,6 +400,25 @@ class WhitePalace(Choice):
default = 0
class DeathLink(Choice):
"""
When you die, everyone dies. Of course the reverse is true too.
When enabled, choose how incoming deathlinks are handled:
vanilla: DeathLink kills you and is just like any other death. RIP your previous shade and geo.
shadeless: DeathLink kills you, but no shade spawns and no geo is lost. Your previous shade, if any, is untouched.
shade: DeathLink functions like a normal death if you do not already have a shade, shadeless otherwise.
"""
option_off = 0
alias_false = 0
alias_no = 0
alias_true = 1
alias_on = 1
alias_yes = 1
option_shadeless = 1
option_vanilla = 2
option_shade = 3
class StartingGeo(Range):
"""The amount of starting geo you have."""
display_name = "Starting Geo"
@@ -296,22 +427,70 @@ class StartingGeo(Range):
default = 0
class CostSanity(Choice):
"""If enabled, most locations with costs (like stag stations) will have randomly determined costs.
If set to shopsonly, CostSanity will only apply to shops (including Grubfather, Seer and Egg Shop).
If set to notshops, CostSanity will only apply to non-shops (e.g. Stag stations and Cornifer locations)
These costs can be in Geo (except Grubfather, Seer and Eggshop), Grubs, Charms, Essence and/or Rancid Eggs
"""
option_off = 0
alias_false = 0
alias_no = 0
option_on = 1
alias_true = 1
alias_yes = 1
option_shopsonly = 2
option_notshops = 3
display_name = "Cost Sanity"
class CostSanityHybridChance(Range):
"""The chance that a CostSanity cost will include two components instead of one, e.g. Grubs + Essence"""
range_end = 100
default = 10
cost_sanity_weights: typing.Dict[str, type(Option)] = {}
for term, cost in cost_terms.items():
option_name = f"CostSanity{cost.option}Weight"
extra_data = {
"__module__": __name__, "range_end": 1000,
"__doc__": (
f"The likelihood of Costsanity choosing a {cost.option} cost."
" Chosen as a sum of all weights from other types."
),
"default": cost.weight
}
if cost == 'GEO':
extra_data["__doc__"] += " Geo costs will never be chosen for Grubfather, Seer, or Egg Shop."
option = type(option_name, (Range,), extra_data)
globals()[option.__name__] = option
cost_sanity_weights[option.__name__] = option
hollow_knight_options: typing.Dict[str, type(Option)] = {
**hollow_knight_randomize_options,
RandomizeElevatorPass.__name__: RandomizeElevatorPass,
**hollow_knight_logic_options,
StartLocation.__name__: StartLocation,
MinimumGrubPrice.__name__: MinimumGrubPrice,
MaximumGrubPrice.__name__: MaximumGrubPrice,
MinimumEssencePrice.__name__: MinimumEssencePrice,
MaximumEssencePrice.__name__: MaximumEssencePrice,
MinimumCharmPrice.__name__: MinimumCharmPrice,
MaximumCharmPrice.__name__: MaximumCharmPrice,
RandomCharmCosts.__name__: RandomCharmCosts,
PlandoCharmCosts.__name__: PlandoCharmCosts,
MinimumEggPrice.__name__: MinimumEggPrice,
MaximumEggPrice.__name__: MaximumEggPrice,
EggShopSlots.__name__: EggShopSlots,
Goal.__name__: Goal,
WhitePalace.__name__: WhitePalace,
StartingGeo.__name__: StartingGeo,
**{
option.__name__: option
for option in (
StartLocation, Goal, WhitePalace, StartingGeo, DeathLink,
MinimumGeoPrice, MaximumGeoPrice,
MinimumGrubPrice, MaximumGrubPrice,
MinimumEssencePrice, MaximumEssencePrice,
MinimumCharmPrice, MaximumCharmPrice,
RandomCharmCosts, PlandoCharmCosts,
MinimumEggPrice, MaximumEggPrice, EggShopSlots,
SlyShopSlots, SlyKeyShopSlots, IseldaShopSlots,
SalubraShopSlots, SalubraCharmShopSlots,
LegEaterShopSlots, GrubfatherRewardSlots,
SeerRewardSlots, ExtraShopSlots,
SplitCrystalHeart, SplitMothwingCloak, SplitMantisClaw,
CostSanity, CostSanityHybridChance,
)
},
**cost_sanity_weights
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,16 +2,19 @@ from __future__ import annotations
import logging
import typing
from collections import Counter
from copy import deepcopy
import itertools
import operator
logger = logging.getLogger("Hollow Knight")
from .Items import item_table, lookup_type_to_names, item_name_groups
from .Regions import create_regions
from .Rules import set_rules
from .Options import hollow_knight_options, hollow_knight_randomize_options, disabled, Goal, WhitePalace
from .Rules import set_rules, cost_terms
from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \
shop_to_option
from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \
event_names, item_effects, connectors, one_ways
event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs
from .Charms import names as charm_names
from BaseClasses import Region, Entrance, Location, MultiWorld, Item, RegionType, LocationProgressType, Tutorial, ItemClassification
@@ -98,6 +101,26 @@ logicless_options = {
"RandomizeLoreTablets", "RandomizeSoulTotems",
}
# Options that affect vanilla starting items
randomizable_starting_items: typing.Dict[str, typing.Tuple[str, ...]] = {
"RandomizeFocus": ("Focus",),
"RandomizeSwim": ("Swim",),
"RandomizeNail": ('Upslash', 'Leftslash', 'Rightslash')
}
# Shop cost types.
shop_cost_types: typing.Dict[str, typing.Tuple[str, ...]] = {
"Egg_Shop": ("RANCIDEGGS",),
"Grubfather": ("GRUBS",),
"Seer": ("ESSENCE",),
"Salubra_(Requires_Charms)": ("CHARMS", "GEO"),
"Sly": ("GEO",),
"Sly_(Key)": ("GEO",),
"Iselda": ("GEO",),
"Salubra": ("GEO",),
"Leg_Eater": ("GEO",),
}
class HKWeb(WebWorld):
tutorials = [Tutorial(
@@ -109,6 +132,8 @@ class HKWeb(WebWorld):
["Ijwu"]
)]
bug_report_page = "https://github.com/Ijwu/Archipelago.HollowKnight/issues/new?assignees=&labels=bug%2C+needs+investigation&template=bug_report.md&title="
class HKWorld(World):
"""Beneath the fading town of Dirtmouth sleeps a vast, ancient kingdom. Many are drawn beneath the surface,
@@ -127,19 +152,18 @@ class HKWorld(World):
item_name_groups = item_name_groups
ranges: typing.Dict[str, typing.Tuple[int, int]]
shops: typing.Dict[str, str] = {
"Egg_Shop": "Egg",
"Grubfather": "Grub",
"Seer": "Essence",
"Salubra_(Requires_Charms)": "Charm"
}
charm_costs: typing.List[int]
cached_filler_items = {}
data_version = 2
def __init__(self, world, player):
super(HKWorld, self).__init__(world, player)
self.created_multi_locations: typing.Dict[str, int] = Counter()
self.created_multi_locations: typing.Dict[str, typing.List[HKLocation]] = {
location: list() for location in multi_locations
}
self.ranges = {}
self.created_shop_items = 0
self.vanilla_shop_costs = deepcopy(vanilla_shop_costs)
def generate_early(self):
world = self.world
@@ -147,16 +171,14 @@ class HKWorld(World):
self.charm_costs = world.PlandoCharmCosts[self.player].get_costs(charm_costs)
# world.exclude_locations[self.player].value.update(white_palace_locations)
world.local_items[self.player].value.add("Mimic_Grub")
for vendor, unit in self.shops.items():
mini = getattr(world, f"Minimum{unit}Price")[self.player]
maxi = getattr(world, f"Maximum{unit}Price")[self.player]
for term, data in cost_terms.items():
mini = getattr(world, f"Minimum{data.option}Price")[self.player]
maxi = getattr(world, f"Maximum{data.option}Price")[self.player]
# if minimum > maximum, set minimum to maximum
mini.value = min(mini.value, maxi.value)
self.ranges[unit] = mini.value, maxi.value
self.ranges[term] = mini.value, maxi.value
world.push_precollected(HKItem(starts[world.StartLocation[self.player].current_key],
True, None, "Event", self.player))
for option_name in disabled:
getattr(world, option_name)[self.player].value = 0
def white_palace_exclusions(self):
exclusions = set()
@@ -199,55 +221,200 @@ class HKWorld(World):
menu_region.locations.append(loc)
def create_items(self):
unfilled_locations = 0
# Generate item pool and associated locations (paired in HK)
pool: typing.List[HKItem] = []
geo_replace: typing.Set[str] = set()
if self.world.RemoveSpellUpgrades[self.player]:
geo_replace.add("Abyss_Shriek")
geo_replace.add("Shade_Soul")
geo_replace.add("Descending_Dark")
wp_exclusions = self.white_palace_exclusions()
junk_replace: typing.Set[str] = set()
if self.world.RemoveSpellUpgrades[self.player]:
junk_replace.update(("Abyss_Shriek", "Shade_Soul", "Descending_Dark"))
randomized_starting_items = set()
for attr, items in randomizable_starting_items.items():
if getattr(self.world, attr)[self.player]:
randomized_starting_items.update(items)
# noinspection PyShadowingNames
def _add(item_name: str, location_name: str):
"""
Adds a pairing of an item and location, doing appropriate checks to see if it should be vanilla or not.
"""
nonlocal unfilled_locations
vanilla = not randomized
excluded = False
if not vanilla and location_name in wp_exclusions:
if location_name == 'King_Fragment':
excluded = True
else:
vanilla = True
if item_name in junk_replace:
item_name = self.get_filler_item_name()
item = self.create_item(item_name)
if location_name == "Start":
if item_name in randomized_starting_items:
if item_name == "Focus":
self.create_location("Focus")
unfilled_locations += 1
pool.append(item)
else:
self.world.push_precollected(item)
return
if vanilla:
location = self.create_vanilla_location(location_name, item)
else:
pool.append(item)
if location_name in multi_locations: # Create shop locations later.
return
location = self.create_location(location_name)
unfilled_locations += 1
if excluded:
location.progress_type = LocationProgressType.EXCLUDED
for option_key, option in hollow_knight_randomize_options.items():
randomized = getattr(self.world, option_key)[self.player]
for item_name, location_name in zip(option.items, option.locations):
vanilla = not randomized
excluded = False
if item_name in geo_replace:
item_name = "Geo_Rock-Default"
item = self.create_item(item_name)
if location_name == "Start":
self.world.push_precollected(item)
if item_name in junk_replace:
item_name = self.get_filler_item_name()
if (item_name == "Crystal_Heart" and self.world.SplitCrystalHeart[self.player]) or \
(item_name == "Mothwing_Cloak" and self.world.SplitMothwingCloak[self.player]):
_add("Left_" + item_name, location_name)
_add("Right_" + item_name, "Split_" + location_name)
continue
location = self.create_location(location_name)
if not vanilla and location_name in wp_exclusions:
if location_name == 'King_Fragment':
excluded = True
if item_name == "Mantis_Claw" and self.world.SplitMantisClaw[self.player]:
_add("Left_" + item_name, "Left_" + location_name)
_add("Right_" + item_name, "Right_" + location_name)
continue
if item_name == "Shade_Cloak" and self.world.SplitMothwingCloak[self.player]:
if self.world.random.randint(0, 1):
item_name = "Left_Mothwing_Cloak"
else:
vanilla = True
if excluded:
location.progress_type = LocationProgressType.EXCLUDED
if vanilla:
location.place_locked_item(item)
else:
pool.append(item)
item_name = "Right_Mothwing_Cloak"
for i in range(self.world.EggShopSlots[self.player].value):
self.create_location("Egg_Shop")
pool.append(self.create_item("Geo_Rock-Default"))
_add(item_name, location_name)
if self.world.RandomizeElevatorPass[self.player]:
randomized = True
_add("Elevator_Pass", "Elevator_Pass")
for shop, locations in self.created_multi_locations.items():
for _ in range(len(locations), getattr(self.world, shop_to_option[shop])[self.player].value):
loc = self.create_location(shop)
unfilled_locations += 1
# Balance the pool
item_count = len(pool)
additional_shop_items = max(item_count - unfilled_locations, self.world.ExtraShopSlots[self.player].value)
# Add additional shop items, as needed.
if additional_shop_items > 0:
shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16)
if not self.world.EggShopSlots[self.player].value: # No eggshop, so don't place items there
shops.remove('Egg_Shop')
for _ in range(additional_shop_items):
shop = self.world.random.choice(shops)
loc = self.create_location(shop)
unfilled_locations += 1
if len(self.created_multi_locations[shop]) >= 16:
shops.remove(shop)
if not shops:
break
# Create filler items, if needed
if item_count < unfilled_locations:
pool.extend(self.create_item(self.get_filler_item_name()) for _ in range(unfilled_locations - item_count))
self.world.itempool += pool
self.apply_costsanity()
self.sort_shops_by_cost()
for shopname in self.shops:
prices: typing.List[int] = []
locations: typing.List[HKLocation] = []
for x in range(1, self.created_multi_locations[shopname]+1):
loc = self.world.get_location(self.get_multi_location_name(shopname, x), self.player)
locations.append(loc)
prices.append(loc.cost)
prices.sort()
for loc, price in zip(locations, prices):
loc.cost = price
def sort_shops_by_cost(self):
for shop, locations in self.created_multi_locations.items():
randomized_locations = list(loc for loc in locations if not loc.vanilla)
prices = sorted(
(loc.costs for loc in randomized_locations),
key=lambda costs: (len(costs),) + tuple(costs.values())
)
for loc, costs in zip(randomized_locations, prices):
loc.costs = costs
def apply_costsanity(self):
setting = self.world.CostSanity[self.player].value
if not setting:
return # noop
def _compute_weights(weights: dict, desc: str) -> typing.Dict[str, int]:
if all(x == 0 for x in weights.values()):
logger.warning(
f"All {desc} weights were zero for {self.world.player_name[self.player]}."
f" Setting them to one instead."
)
weights = {k: 1 for k in weights}
return {k: v for k, v in weights.items() if v}
random = self.world.random
hybrid_chance = getattr(self.world, f"CostSanityHybridChance")[self.player].value
weights = {
data.term: getattr(self.world, f"CostSanity{data.option}Weight")[self.player].value
for data in cost_terms.values()
}
weights_geoless = dict(weights)
del weights_geoless["GEO"]
weights = _compute_weights(weights, "CostSanity")
weights_geoless = _compute_weights(weights_geoless, "Geoless CostSanity")
if hybrid_chance > 0:
if len(weights) == 1:
logger.warning(
f"Only one cost type is available for CostSanity in {self.world.player_name[self.player]}'s world."
f" CostSanityHybridChance will not trigger."
)
if len(weights_geoless) == 1:
logger.warning(
f"Only one cost type is available for CostSanity in {self.world.player_name[self.player]}'s world."
f" CostSanityHybridChance will not trigger in geoless locations."
)
for region in self.world.get_regions(self.player):
for location in region.locations:
if location.vanilla:
continue
if not location.costs:
continue
if location.name == "Vessel_Fragment-Basin":
continue
if setting == CostSanity.option_notshops and location.basename in multi_locations:
continue
if setting == CostSanity.option_shopsonly and location.basename not in multi_locations:
continue
if location.basename in {'Grubfather', 'Seer', 'Eggshop'}:
our_weights = dict(weights_geoless)
else:
our_weights = dict(weights)
rolls = 1
if random.randrange(100) < hybrid_chance:
rolls = 2
if rolls > len(our_weights):
terms = list(our_weights.keys()) # Can't randomly choose cost types, using all of them.
else:
terms = []
for _ in range(rolls):
term = random.choices(list(our_weights.keys()), list(our_weights.values()))[0]
del our_weights[term]
terms.append(term)
location.costs = {term: random.randint(*self.ranges[term]) for term in terms}
location.sort_costs()
def set_rules(self):
world = self.world
@@ -280,12 +447,24 @@ class HKWorld(World):
# 32 bit int
slot_data["seed"] = self.world.slot_seeds[self.player].randint(-2147483647, 2147483646)
for shop, unit in self.shops.items():
slot_data[f"{unit}_costs"] = {
f"{shop}_{i}":
self.world.get_location(f"{shop}_{i}", self.player).cost
for i in range(1, 1 + self.created_multi_locations[shop])
}
# Backwards compatibility for shop cost data (HKAP < 0.1.0)
if not self.world.CostSanity[self.player]:
for shop, terms in shop_cost_types.items():
unit = cost_terms[next(iter(terms))].option
if unit == "Geo":
continue
slot_data[f"{unit}_costs"] = {
loc.name: next(iter(loc.costs.values()))
for loc in self.created_multi_locations[shop]
}
# HKAP 0.1.0 and later cost data.
location_costs = {}
for region in self.world.get_regions(self.player):
for location in region.locations:
if location.costs:
location_costs[location.name] = location.costs
slot_data["location_costs"] = location_costs
slot_data["notch_costs"] = self.charm_costs
@@ -295,30 +474,52 @@ class HKWorld(World):
item_data = item_table[name]
return HKItem(name, item_data.advancement, item_data.id, item_data.type, self.player)
def create_location(self, name: str) -> HKLocation:
unit = self.shops.get(name, None)
if unit:
cost = self.world.random.randint(*self.ranges[unit])
else:
cost = 0
if name in multi_locations:
self.created_multi_locations[name] += 1
name = self.get_multi_location_name(name, self.created_multi_locations[name])
def create_location(self, name: str, vanilla=False) -> HKLocation:
costs = None
basename = name
if name in shop_cost_types:
costs = {
term: self.world.random.randint(*self.ranges[term])
for term in shop_cost_types[name]
}
elif name in vanilla_location_costs:
costs = vanilla_location_costs[name]
multi = self.created_multi_locations.get(name)
if multi is not None:
i = len(multi) + 1
name = f"{name}_{i}"
region = self.world.get_region("Menu", self.player)
loc = HKLocation(self.player, name, self.location_name_to_id[name], region)
if unit:
loc.unit = unit
loc.cost = cost
loc = HKLocation(self.player, name,
self.location_name_to_id[name], region, costs=costs, vanilla=vanilla,
basename=basename)
if multi is not None:
multi.append(loc)
region.locations.append(loc)
return loc
def create_vanilla_location(self, location: str, item: Item):
costs = self.vanilla_shop_costs.get((location, item.name))
location = self.create_location(location, vanilla=True)
location.place_locked_item(item)
if costs:
location.costs = costs.pop()
return location
def collect(self, state, item: HKItem) -> bool:
change = super(HKWorld, self).collect(state, item)
if change:
for effect_name, effect_value in item_effects.get(item.name, {}).items():
state.prog_items[effect_name, item.player] += effect_value
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
if state.prog_items.get(('RIGHTDASH', item.player), 0) and \
state.prog_items.get(('LEFTDASH', item.player), 0):
(state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player]) = \
([max(state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player])] * 2)
return change
def remove(self, state, item: HKItem) -> bool:
@@ -348,17 +549,40 @@ class HKWorld(World):
name = world.get_player_name(player)
spoiler_handle.write(f'\n{name}\n')
hk_world: HKWorld = world.worlds[player]
for shop_name, unit_name in cls.shops.items():
for x in range(1, hk_world.created_multi_locations[shop_name]+1):
loc = world.get_location(hk_world.get_multi_location_name(shop_name, x), player)
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost} {unit_name}")
if world.CostSanity[player].value:
for loc in sorted(
(
loc for loc in itertools.chain(*(region.locations for region in world.get_regions(player)))
if loc.costs
), key=operator.attrgetter('name')
):
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}")
else:
for shop_name, locations in hk_world.created_multi_locations.items():
for loc in locations:
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}")
def get_multi_location_name(self, base: str, i: typing.Optional[int]) -> str:
if i is None:
i = self.created_multi_locations[base]
assert 0 < i < 18, "limited number of multi location IDs reserved."
i = len(self.created_multi_locations[base]) + 1
assert 1 <= 16, "limited number of multi location IDs reserved."
return f"{base}_{i}"
def get_filler_item_name(self) -> str:
if self.player not in self.cached_filler_items:
fillers = ["One_Geo", "Soul_Refill"]
exclusions = self.white_palace_exclusions()
for group in (
'RandomizeGeoRocks', 'RandomizeSoulTotems', 'RandomizeLoreTablets', 'RandomizeJunkPitChests',
'RandomizeRancidEggs'
):
if getattr(self.world, group):
fillers.extend(item for item in hollow_knight_randomize_options[group].items if item not in
exclusions)
self.cached_filler_items[self.player] = fillers
return self.world.random.choice(self.cached_filler_items[self.player])
def create_region(world: MultiWorld, player: int, name: str, location_names=None, exits=None) -> Region:
ret = Region(name, RegionType.Generic, name, player)
@@ -376,11 +600,34 @@ def create_region(world: MultiWorld, player: int, name: str, location_names=None
class HKLocation(Location):
game: str = "Hollow Knight"
cost: int = 0
costs: typing.Dict[str, int] = None
unit: typing.Optional[str] = None
vanilla = False
basename: str
def __init__(self, player: int, name: str, code=None, parent=None):
def sort_costs(self):
if self.costs is None:
return
self.costs = {k: self.costs[k] for k in sorted(self.costs.keys(), key=lambda x: cost_terms[x].sort)}
def __init__(
self, player: int, name: str, code=None, parent=None,
costs: typing.Dict[str, int] = None, vanilla: bool = False, basename: str = None
):
self.basename = basename or name
super(HKLocation, self).__init__(player, name, code if code else None, parent)
self.vanilla = vanilla
if costs:
self.costs = dict(costs)
self.sort_costs()
def cost_text(self, separator=" and "):
if self.costs is None:
return None
return separator.join(
f"{value} {cost_terms[term].singular if value == 1 else cost_terms[term].plural}"
for term, value in self.costs.items()
)
class HKItem(Item):
@@ -393,6 +640,10 @@ class HKItem(Item):
classification = ItemClassification.progression_skip_balancing
elif type == "Charm" and name not in progression_charms:
classification = ItemClassification.progression_skip_balancing
elif type in ("Map", "Journal"):
classification = ItemClassification.filler
elif type in ("Mask", "Ore", "Vessel"):
classification = ItemClassification.useful
elif advancement:
classification = ItemClassification.progression
else:

View File

@@ -1,50 +1,20 @@
from ..generic.Rules import set_rule, add_rule
# This module is written by Extractor.py, do not edit manually!.
from functools import partial
units = {
"Egg": "RANCIDEGGS",
"Grub": "GRUBS",
"Essence": "ESSENCE",
"Charm": "CHARMS",
}
def hk_set_rule(hk_world, location: str, rule):
count = hk_world.created_multi_locations[location]
if count:
locations = [f"{location}_{x}" for x in range(1, count+1)]
elif (location, hk_world.player) in hk_world.world._location_cache:
locations = [location]
else:
return
for location in locations:
set_rule(hk_world.world.get_location(location, hk_world.player), rule)
def set_shop_prices(hk_world):
def set_generated_rules(hk_world, hk_set_rule):
player = hk_world.player
for shop, unit in hk_world.shops.items():
for i in range(1, 1 + hk_world.created_multi_locations[shop]):
loc = hk_world.world.get_location(f"{shop}_{i}", hk_world.player)
add_rule(loc, lambda state, unit=units[unit], cost=loc.cost: state.count(unit, player) >= cost)
def set_rules(hk_world):
player = hk_world.player
world = hk_world.world
fn = partial(hk_set_rule, hk_world)
# Events
{% for location, rule_text in event_rules.items() %}
hk_set_rule(hk_world, "{{location}}", lambda state: {{rule_text}})
fn("{{location}}", lambda state: {{rule_text}})
{%- endfor %}
# Locations
{% for location, rule_text in location_rules.items() %}
hk_set_rule(hk_world, "{{location}}", lambda state: {{rule_text}})
fn("{{location}}", lambda state: {{rule_text}})
{%- endfor %}
# Shop prices
set_shop_prices(hk_world)
# Connectors
{% for entrance, rule_text in connectors_rules.items() %}
rule = lambda state: {{rule_text}}
@@ -54,4 +24,4 @@ def set_rules(hk_world):
world.get_entrance("{{entrance}}_R", player).access_rule = lambda state, entrance= entrance: \
rule(state) and entrance.can_reach(state)
{%- endif %}
{% endfor %}
{%- endfor %}

View File

@@ -47,16 +47,29 @@ When the console tells you that you have joined the room, you're all set. Congra
multiworld game! At this point any additional minecraft players may connect to your forge server. To start the game once
everyone is ready use the command `/start`.
## Manual Installation
## Non-Windows Installation
The Minecraft Client will install forge and the mod for other operating systems but Java has to be provided by the
user. Head to [minecraft_versions.json on the MC AP GitHub](https://raw.githubusercontent.com/KonoTyran/Minecraft_AP_Randomizer/master/versions/minecraft_versions.json)
to see which java version is required. New installations will default to the topmost "release" version.
- Install the matching Amazon Corretto JDK
- see [Manual Installation Software Links](#manual-installation-software-links)
- or package manager provided by your OS / distribution
- Open your `host.yaml` and add the path to your Java below the `minecraft_options` key
- ` java: "path/to/java-xx-amazon-corretto/bin/java"`
- Run the Minecraft Client and select your .apmc file
## Full Manual Installation
It is highly recommended to ues the Archipelago installer to handle the installation of the forge server for you.
support will not be given for those wishing to manually install forge. For those of you who know how, and wish to do so,
Support will not be given for those wishing to manually install forge. For those of you who know how, and wish to do so,
the following links are the versions of the software we use.
### Manual install Software links
### Manual Installation Software Links
- [Minecraft Forge Download Page](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.18.2.html)
- [Minecraft Forge Download Page](https://files.minecraftforge.net/net/minecraftforge/forge/)
- [Minecraft Archipelago Randomizer Mod Releases Page](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
- **DO NOT INSTALL THIS ON YOUR CLIENT**
- [Amazon Corretto Java 17 Download Page](https://docs.aws.amazon.com/corretto/latest/corretto-17-ug/downloads-list.html)
- [Amazon Corretto](https://docs.aws.amazon.com/corretto/)
- pick the matching version and select "Downloads" on the left

View File

@@ -1088,10 +1088,10 @@ def get_pool_core(world):
placed_items['Hideout Jail Guard (4 Torches)'] = 'Recovery Heart'
skip_in_spoiler_locations.extend(['Hideout Jail Guard (2 Torches)', 'Hideout Jail Guard (3 Torches)', 'Hideout Jail Guard (4 Torches)'])
else:
placed_items['Hideout Jail Guard (1 Torch)'] = 'Small Key (Gerudo Fortress)'
placed_items['Hideout Jail Guard (2 Torches)'] = 'Small Key (Gerudo Fortress)'
placed_items['Hideout Jail Guard (3 Torches)'] = 'Small Key (Gerudo Fortress)'
placed_items['Hideout Jail Guard (4 Torches)'] = 'Small Key (Gerudo Fortress)'
placed_items['Hideout Jail Guard (1 Torch)'] = 'Small Key (Thieves Hideout)'
placed_items['Hideout Jail Guard (2 Torches)'] = 'Small Key (Thieves Hideout)'
placed_items['Hideout Jail Guard (3 Torches)'] = 'Small Key (Thieves Hideout)'
placed_items['Hideout Jail Guard (4 Torches)'] = 'Small Key (Thieves Hideout)'
if world.shuffle_gerudo_card and world.gerudo_fortress != 'open':
pool.append('Gerudo Membership Card')

View File

@@ -51,20 +51,20 @@ known_logic_tricks = {
Can be reached by side-hopping off
the watchtower.
'''},
'Dodongo\'s Cavern Staircase with Bow': {
"Dodongo's Cavern Staircase with Bow": {
'name' : 'logic_dc_staircase',
'tags' : ("Dodongo's Cavern",),
'tooltip' : '''\
The Bow can be used to knock down the stairs
with two well-timed shots.
'''},
'Dodongo\'s Cavern Spike Trap Room Jump without Hover Boots': {
"Dodongo's Cavern Spike Trap Room Jump without Hover Boots": {
'name' : 'logic_dc_jump',
'tags' : ("Dodongo's Cavern",),
'tooltip' : '''\
Jump is adult only.
'''},
'Dodongo\'s Cavern Vines GS from Below with Longshot': {
"Dodongo's Cavern Vines GS from Below with Longshot": {
'name' : 'logic_dc_vines_gs',
'tags' : ("Dodongo's Cavern", "Skulltulas",),
'tooltip' : '''\
@@ -73,7 +73,7 @@ known_logic_tricks = {
from below, by shooting it through the vines,
bypassing the need to lower the staircase.
'''},
'Thieves\' Hideout "Kitchen" with No Additional Items': {
'''Thieves' Hideout "Kitchen" with No Additional Items''': {
'name' : 'logic_gerudo_kitchen',
'tags' : ("Thieves' Hideout", "Gerudo's Fortress"),
'tooltip' : '''\
@@ -157,7 +157,7 @@ known_logic_tricks = {
Can jump up to the spinning platform from
below as adult.
'''},
'Crater\'s Bean PoH with Hover Boots': {
"Crater's Bean PoH with Hover Boots": {
'name' : 'logic_crater_bean_poh_with_hovers',
'tags' : ("Death Mountain Crater",),
'tooltip' : '''\
@@ -165,7 +165,7 @@ known_logic_tricks = {
near Goron City and walk up the
very steep slope.
'''},
'Zora\'s Domain Entry with Cucco': {
"Zora's Domain Entry with Cucco": {
'name' : 'logic_zora_with_cucco',
'tags' : ("Zora's River",),
'tooltip' : '''\
@@ -404,7 +404,7 @@ known_logic_tricks = {
Longshot can be shot through the ceiling to obtain
the token with two fewer small keys than normal.
'''},
'Zora\'s River Lower Freestanding PoH as Adult with Nothing': {
"Zora's River Lower Freestanding PoH as Adult with Nothing": {
'name' : 'logic_zora_river_lower',
'tags' : ("Zora's River",),
'tooltip' : '''\
@@ -502,7 +502,7 @@ known_logic_tricks = {
you can get enough of a break to take some time
to aim more carefully.
'''},
'Dodongo\'s Cavern Scarecrow GS with Armos Statue': {
"Dodongo's Cavern Scarecrow GS with Armos Statue": {
'name' : 'logic_dc_scarecrow_gs',
'tags' : ("Dodongo's Cavern", "Skulltulas",),
'tooltip' : '''\
@@ -541,7 +541,7 @@ known_logic_tricks = {
'name' : 'logic_spirit_mq_lower_adult',
'tags' : ("Spirit Temple",),
'tooltip' : '''\
It can be done with Din\'s Fire and Bow.
It can be done with Din's Fire and Bow.
Whenever an arrow passes through a lit torch, it
resets the timer. It's finicky but it's also
possible to stand on the pillar next to the center
@@ -704,13 +704,13 @@ known_logic_tricks = {
in the same jump in order to destroy it before you
fall into the lava.
'''},
'Zora\'s Domain Entry with Hover Boots': {
"Zora's Domain Entry with Hover Boots": {
'name' : 'logic_zora_with_hovers',
'tags' : ("Zora's River",),
'tooltip' : '''\
Can hover behind the waterfall as adult.
'''},
'Zora\'s Domain GS with No Additional Items': {
"Zora's Domain GS with No Additional Items": {
'name' : 'logic_domain_gs',
'tags' : ("Zora's Domain", "Skulltulas",),
'tooltip' : '''\
@@ -736,7 +736,7 @@ known_logic_tricks = {
needing a Bow.
Applies in both vanilla and MQ Shadow.
'''},
'Stop Link the Goron with Din\'s Fire': {
"Stop Link the Goron with Din's Fire": {
'name' : 'logic_link_goron_dins',
'tags' : ("Goron City",),
'tooltip' : '''\
@@ -825,7 +825,7 @@ known_logic_tricks = {
Link will not be expected to do anything at Gerudo's
Fortress.
'''},
'Zora\'s River Upper Freestanding PoH as Adult with Nothing': {
"Zora's River Upper Freestanding PoH as Adult with Nothing": {
'name' : 'logic_zora_river_upper',
'tags' : ("Zora's River",),
'tooltip' : '''\
@@ -971,7 +971,7 @@ known_logic_tricks = {
in the Water Temple are not going to be relevant unless this
trick is first enabled.
'''},
'Water Temple Central Pillar GS with Farore\'s Wind': {
"Water Temple Central Pillar GS with Farore's Wind": {
'name' : 'logic_water_central_gs_fw',
'tags' : ("Water Temple", "Skulltulas",),
'tooltip' : '''\
@@ -1104,7 +1104,7 @@ known_logic_tricks = {
this allows you to obtain the GS on the door frame
as adult without Hookshot or Song of Time.
'''},
'Dodongo\'s Cavern MQ Early Bomb Bag Area as Child': {
"Dodongo's Cavern MQ Early Bomb Bag Area as Child": {
'name' : 'logic_dc_mq_child_bombs',
'tags' : ("Dodongo's Cavern",),
'tooltip' : '''\
@@ -1113,7 +1113,7 @@ known_logic_tricks = {
without needing a Slingshot. You will
take fall damage.
'''},
'Dodongo\'s Cavern Two Scrub Room with Strength': {
"Dodongo's Cavern Two Scrub Room with Strength": {
'name' : 'logic_dc_scrub_room',
'tags' : ("Dodongo's Cavern",),
'tooltip' : '''\
@@ -1122,7 +1122,7 @@ known_logic_tricks = {
destroy the mud wall blocking the room with two
Deku Scrubs.
'''},
'Dodongo\'s Cavern Child Slingshot Skips': {
"Dodongo's Cavern Child Slingshot Skips": {
'name' : 'logic_dc_slingshot_skip',
'tags' : ("Dodongo's Cavern",),
'tooltip' : '''\
@@ -1132,7 +1132,7 @@ known_logic_tricks = {
you also enable the Adult variant: "Dodongo's
Cavern Spike Trap Room Jump without Hover Boots".
'''},
'Dodongo\'s Cavern MQ Light the Eyes with Strength': {
"Dodongo's Cavern MQ Light the Eyes with Strength": {
'name' : 'logic_dc_mq_eyes',
'tags' : ("Dodongo's Cavern",),
'tooltip' : '''\
@@ -1145,7 +1145,7 @@ known_logic_tricks = {
Also, the bombable floor before King Dodongo can be
destroyed with Hammer if hit in the very center.
'''},
'Dodongo\'s Cavern MQ Back Areas as Child without Explosives': {
"Dodongo's Cavern MQ Back Areas as Child without Explosives": {
'name' : 'logic_dc_mq_child_back',
'tags' : ("Dodongo's Cavern",),
'tooltip' : '''\
@@ -1232,7 +1232,7 @@ known_logic_tricks = {
It can also be done as child, using the
Slingshot instead of the Bow.
'''},
'Fire Temple East Tower without Scarecrow\'s Song': {
"Fire Temple East Tower without Scarecrow's Song": {
'name' : 'logic_fire_scarecrow',
'tags' : ("Fire Temple",),
'tooltip' : '''\
@@ -1277,14 +1277,14 @@ known_logic_tricks = {
Removes the requirements for the Lens of Truth
in Bottom of the Well.
'''},
'Ganon\'s Castle MQ without Lens of Truth': {
"Ganon's Castle MQ without Lens of Truth": {
'name' : 'logic_lens_castle_mq',
'tags' : ("Lens of Truth","Ganon's Castle",),
'tooltip' : '''\
Removes the requirements for the Lens of Truth
in Ganon's Castle MQ.
'''},
'Ganon\'s Castle without Lens of Truth': {
"Ganon's Castle without Lens of Truth": {
'name' : 'logic_lens_castle',
'tags' : ("Lens of Truth","Ganon's Castle",),
'tooltip' : '''\

View File

@@ -8,5 +8,5 @@ lookup_id_to_name = {}
for item in location_table:
lookup_id_to_name[item["id"]] = item["name"]
lookup_id_to_name[None] = "Tangaroa Next Frequency"
lookup_id_to_name[None] = "Utopia Complete"
lookup_name_to_id = {name: id for id, name in lookup_id_to_name.items()}

View File

@@ -37,6 +37,20 @@ class IslandFrequencyLocations(Choice):
option_anywhere = 3
default = 1
class IslandGenerationDistance(Choice):
"""Sets how far away islands spawn from you when you input their coordinates into the Receiver."""
display_name = "Island distance"
option_quarter = 2
option_half = 4
option_vanilla = 8
option_double = 16
option_quadrouple = 32
default = 8
class ExpensiveResearch(Toggle):
"""Makes unlocking items in the Crafting Table consume the researched items."""
display_name = "Expensive research"
class ProgressiveItems(DefaultOnToggle):
"""Makes some items, like the Bow and Arrow, progressive rather than raw unlocks."""
display_name = "Progressive items"
@@ -55,6 +69,8 @@ raft_options = {
"maximum_resource_pack_amount": MaximumResourcePackAmount,
"duplicate_items": DuplicateItems,
"island_frequency_locations": IslandFrequencyLocations,
"island_generation_distance": IslandGenerationDistance,
"expensive_research": ExpensiveResearch,
"progressive_items": ProgressiveItems,
"big_island_early_crafting": BigIslandEarlyCrafting,
"paddleboard_mode": PaddleboardMode

View File

@@ -12,6 +12,9 @@ class RaftLogic(LogicMixin):
def raft_can_smelt_items(self, player):
return self.has("Smelter", player)
def raft_can_find_titanium(self, player):
return self.has("Metal detector", player)
def raft_can_craft_bolt(self, player):
return self.raft_can_smelt_items(player) and self.has("Bolt", player)
@@ -76,7 +79,7 @@ class RaftLogic(LogicMixin):
return self.raft_can_craft_battery(player) and self.raft_can_craft_reciever(player) and self.raft_can_craft_antenna(player)
def raft_can_drive(self, player): # The player can go wherever they want with the engine
return self.raft_can_craft_engine(player) and self.raft_can_craft_steeringWheel(player)
return (self.raft_can_craft_engine(player) and self.raft_can_craft_steeringWheel(player)) or self.raft_paddleboard_mode_enabled(player)
def raft_can_access_radio_tower(self, player):
return self.raft_can_navigate(player)
@@ -92,24 +95,42 @@ class RaftLogic(LogicMixin):
def raft_can_access_balboa_island(self, player):
return (self.raft_can_complete_vasagatan(player)
and (self.raft_can_drive(player) or self.raft_paddleboard_mode_enabled(player))
and self.raft_can_drive(player)
and self.has("Balboa Island Frequency", player))
def raft_can_complete_balboa_island(self, player):
return self.raft_can_access_balboa_island(player) and self.raft_can_craft_machete(player) and self.raft_can_fire_bow(player)
return self.raft_can_access_balboa_island(player) and self.raft_can_craft_machete(player)
def raft_can_access_caravan_island(self, player):
return self.raft_can_complete_balboa_island(player) and (self.raft_can_drive(player) or self.raft_paddleboard_mode_enabled(player)) and self.has("Caravan Island Frequency", player)
return self.raft_can_complete_balboa_island(player) and self.raft_can_drive(player) and self.has("Caravan Island Frequency", player)
def raft_can_complete_caravan_island(self, player):
return self.raft_can_access_caravan_island(player) and self.raft_can_craft_ziplineTool(player)
def raft_can_access_tangaroa(self, player):
return self.raft_can_complete_caravan_island(player) and (self.raft_can_drive(player) or self.raft_paddleboard_mode_enabled(player)) and self.has("Tangaroa Frequency", player)
return self.raft_can_complete_caravan_island(player) and self.raft_can_drive(player) and self.has("Tangaroa Frequency", player)
def raft_can_complete_tangaroa(self, player):
return self.raft_can_access_tangaroa(player)
def raft_can_access_varuna_point(self, player):
return self.raft_can_complete_tangaroa(player) and self.raft_can_drive(player) and self.has("Varuna Point Frequency", player)
def raft_can_complete_varuna_point(self, player):
return self.raft_can_access_varuna_point(player)
def raft_can_access_temperance(self, player):
return self.raft_can_complete_varuna_point(player) and self.raft_can_drive(player) and self.has("Temperance Frequency", player)
def raft_can_complete_temperance(self, player):
return self.raft_can_access_temperance(player)
def raft_can_access_utopia(self, player):
return self.raft_can_complete_temperance(player) and self.raft_can_drive(player) and self.has("Utopia Frequency", player)
def raft_can_complete_utopia(self, player):
return self.raft_can_access_utopia(player)
def set_rules(world, player):
regionChecks = {
"Raft": lambda state: True,
@@ -118,7 +139,10 @@ def set_rules(world, player):
"Vasagatan": lambda state: state.raft_can_complete_radio_tower(player) and state.raft_can_access_vasagatan(player),
"BalboaIsland": lambda state: state.raft_can_complete_vasagatan(player) and state.raft_can_access_balboa_island(player),
"CaravanIsland": lambda state: state.raft_can_complete_balboa_island(player) and state.raft_can_access_caravan_island(player),
"Tangaroa": lambda state: state.raft_can_complete_caravan_island(player) and state.raft_can_access_tangaroa(player)
"Tangaroa": lambda state: state.raft_can_complete_caravan_island(player) and state.raft_can_access_tangaroa(player),
"Varuna Point": lambda state: state.raft_can_complete_tangaroa(player) and state.raft_can_access_varuna_point(player),
"Temperance": lambda state: state.raft_can_complete_varuna_point(player) and state.raft_can_access_temperance(player),
"Utopia": lambda state: state.raft_can_complete_temperance(player) and state.raft_can_access_utopia(player)
}
itemChecks = {
"Plank": lambda state: True,
@@ -143,15 +167,14 @@ def set_rules(world, player):
"Hinge": lambda state: state.raft_can_craft_hinge(player),
"CircuitBoard": lambda state: state.raft_can_craft_circuitBoard(player),
"PlasticBottle_Empty": lambda state: state.raft_can_craft_plasticBottle(player),
"Shear": lambda state: state.raft_can_craft_shears(player),
"Wool": lambda state: state.raft_can_capture_animals(player) and state.raft_can_craft_shears(player),
"HoneyComb": lambda state: state.raft_can_access_balboa_island(player),
"Jar_Bee": lambda state: state.raft_can_access_balboa_island(player) and state.raft_can_smelt_items(player),
"Dirt": lambda state: state.raft_can_get_dirt(player),
"Egg": lambda state: state.raft_can_capture_animals(player),
"TitaniumIngot": lambda state: state.raft_can_smelt_items(player) and state.raft_can_find_titanium(player),
# Specific items for story island location checks
"Machete": lambda state: state.raft_can_craft_machete(player),
"BowAndArrow": lambda state: state.raft_can_fire_bow(player),
"Zipline tool": lambda state: state.raft_can_craft_ziplineTool(player)
}

View File

@@ -39,8 +39,8 @@ class RaftWorld(World):
location_name_to_id = locations_lookup_name_to_id
options = raft_options
data_version = 1
required_client_version = (0, 2, 0)
data_version = 2
required_client_version = (0, 3, 4)
def generate_basic(self):
minRPSpecified = self.world.minimum_resource_pack_amount[self.player].value
@@ -96,6 +96,11 @@ class RaftWorld(World):
slot_data = {}
return slot_data
def get_pre_fill_items(self):
if self.world.island_frequency_locations[self.player] in [0, 1]:
return [loc.item for loc in self.world.get_filled_locations()]
return []
def create_item_replaceAsNecessary(self, name: str) -> Item:
isFrequency = "Frequency" in name
shouldUseProgressive = ((isFrequency and self.world.island_frequency_locations[self.player].value == 2)
@@ -132,13 +137,19 @@ class RaftWorld(World):
self.setLocationItem("Vasagatan Frequency to Balboa", "Balboa Island Frequency")
self.setLocationItem("Relay Station quest", "Caravan Island Frequency")
self.setLocationItem("Caravan Island Frequency to Tangaroa", "Tangaroa Frequency")
self.setLocationItem("Tangaroa Frequency to Varuna Point", "Varuna Point Frequency")
self.setLocationItem("Varuna Point Frequency to Temperance", "Temperance Frequency")
self.setLocationItem("Temperance Frequency to Utopia", "Utopia Frequency")
elif self.world.island_frequency_locations[self.player] == 1:
self.setLocationItemFromRegion("RadioTower", "Vasagatan Frequency")
self.setLocationItemFromRegion("Vasagatan", "Balboa Island Frequency")
self.setLocationItemFromRegion("BalboaIsland", "Caravan Island Frequency")
self.setLocationItemFromRegion("CaravanIsland", "Tangaroa Frequency")
self.setLocationItemFromRegion("Tangaroa", "Varuna Point Frequency")
self.setLocationItemFromRegion("Varuna Point", "Temperance Frequency")
self.setLocationItemFromRegion("Temperance", "Utopia Frequency")
# Victory item
self.world.get_location("Tangaroa Next Frequency", self.player).place_locked_item(
self.world.get_location("Utopia Complete", self.player).place_locked_item(
RaftItem("Victory", ItemClassification.progression, None, player=self.player))
def setLocationItem(self, location: str, itemName: str):
@@ -151,6 +162,12 @@ class RaftWorld(World):
self.world.itempool.remove(itemToUse)
location = random.choice(list(loc for loc in location_table if loc["region"] == region))
self.world.get_location(location["name"], self.player).place_locked_item(itemToUse)
def fill_slot_data(self):
return {
"IslandGenerationDistance": self.world.island_generation_distance[self.player].value,
"ExpensiveResearch": self.world.expensive_research[self.player].value
}
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
ret = Region(name, RegionType.Generic, name, player)

View File

@@ -5,10 +5,10 @@ The player settings page for this game is located <a href="../player-settings">h
you need to configure and export a config file.
## What does randomization do to this game?
All of the items from the Research Table, as well as all the note/blueprint pickups from story islands, are changed to location checks. Blueprint items themselves are never given. The Research Table recipes will *remove* the researched items for that recipe once learned, meaning many more resources must be put into the Research Table to get all the unlocks from it.
All of the items from the Research Table, as well as all the note/blueprint/character pickups from story islands, are changed to location checks. Blueprint items themselves are never given when receiving a blueprint.
## What is the goal of Raft when randomized?
The goal remains the same: To pick up the note that has the frequency for the next unreleased story island from Tangaroa.
The goal remains the same: To complete the game by getting to the end of the game and finishing Utopia.
## Which items can be in another player's world?
All of the craftable items from the Research Table and Blueprints, as well as frequencies. Since there are more locations in Raft than there are items to receive, Resource Packs with basic earlygame materials and/or duplicate items may be added to the item pool (configurable).

Some files were not shown because too many files have changed in this diff Show More