From 84ec3d535398d7d000f431c81af40295286f49c0 Mon Sep 17 00:00:00 2001 From: Grrmo <72080072+Grrmo@users.noreply.github.com> Date: Sun, 9 Jan 2022 14:47:28 +0100 Subject: [PATCH 01/32] Corrected typos and wrong information - Game executable names for Linux and Mac were wrong - Fixed some typos and changed grammar and semantics in some places --- .../assets/tutorial/timespinner/setup_en.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/WebHostLib/static/assets/tutorial/timespinner/setup_en.md b/WebHostLib/static/assets/tutorial/timespinner/setup_en.md index a65ff52a..5188a36b 100644 --- a/WebHostLib/static/assets/tutorial/timespinner/setup_en.md +++ b/WebHostLib/static/assets/tutorial/timespinner/setup_en.md @@ -2,29 +2,29 @@ ## Required Software -- [Timespinner (steam)](https://store.steampowered.com/app/368620/Timespinner/), [Timespinner (humble)](https://www.humblebundle.com/store/timespinner) or [Timespinner (GOG)](https://www.gog.com/game/timespinner) (other versions are not supported) +- [Timespinner (Steam)](https://store.steampowered.com/app/368620/Timespinner/), [Timespinner (Humble)](https://www.humblebundle.com/store/timespinner) or [Timespinner (GOG)](https://www.gog.com/game/timespinner) (other versions are not supported) - [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer) ## General Concept -The timespinner Randomizer loads Timespinner.exe from the same folder, and alters its state in memory to allow for randomization of the items +The Timespinner Randomizer loads Timespinner.exe from the same folder, and alters its state in memory to allow for randomization of the items ## Installation Procedures -Download latest release on [Timespinner Randomizer Releases](https://github.com/JarnoWesthof/TsRandomizer/releases) you can find the .zip files on the releases page, download the zip for your current platform. Then extract the zip to the folder where your Timespinner game is installed. Then just run TsRandomizer.exe (on windows) or TsRandomizerItemTracker.bin.x86_64 (on linux) or TsRandomizerItemTracker.bin.osx (on mac) instead of Timespinner.exe to start the game in randomized mode, for more info see the [ReadMe](https://github.com/JarnoWesthof/TsRandomizer) +Download latest release on [Timespinner Randomizer Releases](https://github.com/JarnoWesthof/TsRandomizer/releases) you can find the .zip files on the releases page. Download the zip for your current platform. Then extract the zip to the folder where your Timespinner game is installed. Then just run TsRandomizer.exe (on Windows) or TsRandomizer.bin.x86_64 (on Linux) or TsRandomizer.bin.osx (on Mac) instead of Timespinner.exe to start the game in randomized mode. For more info see the [ReadMe](https://github.com/JarnoWesthof/TsRandomizer) ## Joining a MultiWorld Game 1. Run TsRandomizer.exe 2. Select "New Game" -3. Switch "<< Select Seed >>" to "<< Archiplago >>" by pressing left on the controller or keyboard -4. Select "<< Archiplago >>" to open a new menu where you can enter your Archipelago login credentails +3. Switch "<< Select Seed >>" to "<< Archipelago >>" by pressing left on your controller or keyboard +4. Select "<< Archipelago >>" to open a new menu where you can enter your Archipelago login credentials * NOTE: the input fields support Ctrl + V pasting of values 5. Select "Connect" -6. If all went well you will be taken back the difficulty selection menu and the game will start as soon as you select a difficulty +6. If all went well you will be taken back to the difficulty selection menu and the game will start as soon as you select a difficulty ## Where do I get a config file? -The [Player Settings](https://archipelago.gg/games/Timespinner/player-settings) page on the website allows you to configure your personal settings and export a config file from them. +The [Player Settings](https://archipelago.gg/games/Timespinner/player-settings) page on the website allows you to configure your personal settings and export them into a config file * The Timespinner Randomizer option "StinkyMaw" is currently always enabled for Archipelago generated seeds -* The Timespinner Randomizer options "ProgressiveVerticalMovement" & "ProgressiveKeycards" are currently not supported on Archipelago generated seeds \ No newline at end of file +* The Timespinner Randomizer options "ProgressiveVerticalMovement" & "ProgressiveKeycards" are currently not supported on Archipelago generated seeds From 821f98eb46d65986d492f1bc0a85cb6890c9a713 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 9 Jan 2022 14:13:00 -0600 Subject: [PATCH 02/32] Add a new multi trigger example and explain use of "imaginary" options --- .../tutorial/archipelago/triggers_en.md | 90 ++++++++++++++++--- 1 file changed, 78 insertions(+), 12 deletions(-) diff --git a/WebHostLib/static/assets/tutorial/archipelago/triggers_en.md b/WebHostLib/static/assets/tutorial/archipelago/triggers_en.md index 2e59d827..46d5cd1f 100644 --- a/WebHostLib/static/assets/tutorial/archipelago/triggers_en.md +++ b/WebHostLib/static/assets/tutorial/archipelago/triggers_en.md @@ -9,23 +9,19 @@ that was created using entirely triggers and plando. For more information on pla [this guide](/tutorial/archipelago/plando/en) or [this guide](/tutorial/zelda3/plando/en). ## Trigger use -Triggers have to be defined in the root of the yaml file meaning it must be outside of a game section. -The best place to do this is the bottom of the yaml. -- Triggers comprise of the trigger section and then each trigger must have an `option_category`, `option_name`, and -`option_result` from which it will react to and then an `options` section where the definition of what will happen. +Triggers may be defined in either the root or in the relevant game sections. Generally, The best place to do this is the bottom of the yaml for clear organization. +- Triggers comprise the trigger section and then each trigger must have an `option_category`, `option_name`, and +`option_result` from which it will react to and then an `options` section for the definition of what will happen. - `option_category` is the defining section from which the option is defined in. - Example: `A Link to the Past` - - This is the root category the option is located in. If the option you're triggering off of is in root then you -would use `null`, otherwise this is the game for which you want this option trigger to activate. + - This is the root category the option is located in. If the option you're triggering off of is in root then you would use `null`, otherwise this is the game for which you want this option trigger to activate. - `option_name` is the option setting from which the triggered choice is going to react to. - Example: `shop_item_slots` - - This can be any option from any category defined in the yaml file in either root or a game section except for `game`. + - This can be any option from any category defined in the yaml file in either root or a game section. - `option_result` is the result of this option setting from which you would like to react. - Example: `15` - - Each trigger must be used for exactly one option result. If you would like the same thing to occur with multiple -results you would need multiple triggers for this. -- `options` is where you define what will happen when this is detected. This can be something as simple as ensuring -another option also gets selected or placing an item in a certain location. + - Each trigger must be used for exactly one option result. If you would like the same thing to occur with multiple results you would need multiple triggers for this. +- `options` is where you define what will happen when this is detected. This can be something as simple as ensuring another option also gets selected or placing an item in a certain location. It is possible to have multiple things happen in this section. - Example: ```yaml A Link to the Past: @@ -66,4 +62,74 @@ For example: Timespinner: Inverted: true ``` -In this example if your world happens to roll SpecificKeycards then your game will also start in inverted. \ No newline at end of file +In this example if your world happens to roll SpecificKeycards then your game will also start in inverted. + +It is also possible to use imaginary names in options to trigger specific settings. You can use these made up names in either your main options or to trigger from another trigger. Currently, this is the only way to trigger on "setting 1 AND setting 2". + +For example: + ```yaml + triggers: + - option_category: Secret of Evermore + option_name: doggomizer + option_result: pupdunk + options: + Secret of Evermore: + difficulty: + normal: 50 + pupdunk_hard: 25 + pupdunk_mystery: 25 + exp_modifier: + 150: 50 + 200: 50 + - option_category: Secret of Evermore + option_name: difficulty + option_result: pupdunk_hard + options: + Secret of Evermore: + fix_wings_glitch: false + difficulty: hard + - option_category: Secret of Evermore + option_name: difficulty + option_result: pupdunk_mystery + options: + Secret of Evermore: + fix_wings_glitch: false + difficulty: mystery + ``` + +In this example if the `pupdunk` option is rolled then the difficulty values will be rolled again using the new options `normal`, `pupdunk_hard`, and `pupdunk_mystery`, and the exp modifier will be rerolled using new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard` and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery". + +It is also possible to use imaginary names in options to trigger specific settings. You can use these made up names in either your main options or to trigger from another trigger. Currently this is the only way to trigger on "setting 1 AND setting 2". + +For example: + ```yaml + triggers: + - option_category: Secret of Evermore + option_name: doggomizer + option_result: pupdunk + options: + Secret of Evermore: + difficulty: + normal: 50 + pupdunk_hard: 25 + pupdunk_mystery: 25 + exp_modifier: + 150: 50 + 200: 50 + - option_category: Secret of Evermore + option_name: difficulty + option_result: pupdunk_hard + options: + Secret of Evermore: + fix_wings_glitch: false + difficulty: hard + - option_category: Secret of Evermore + option_name: difficulty + option_result: pupdunk_mystery + options: + Secret of Evermore: + fix_wings_glitch: false + difficulty: mystery + ``` + +In this example if the `pupdunk` option is rolled then the difficulty values will be rolled again using the new options `normal`, `pupdunk_hard`, and `pupdunk_mystery`, and the exp modifier will be rerolled using new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard` and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery". \ No newline at end of file From 061de66397845ef89ccb6b2ecd748d164ccff0b2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 9 Jan 2022 23:15:41 +0100 Subject: [PATCH 03/32] MultiServer: don't mark a slot as having Activity if a location check was done through Collect --- MultiServer.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index f92077ad..12845121 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -639,7 +639,7 @@ def collect_player(ctx: Context, team: int, slot: int): ctx.notify_all("%s (Team #%d) has collected" % (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) + register_location_checks(ctx, team, source_player, location_ids, count_activity=False) update_checked_locations(ctx, team, source_player) @@ -651,11 +651,13 @@ def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]: return sorted(items) -def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int]): +def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int], + count_activity: bool = True): new_locations = set(locations) - ctx.location_checks[team, slot] new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata if new_locations: - ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc) + if count_activity: + ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc) for location in new_locations: item_id, target_player = ctx.locations[slot][location] new_item = NetworkItem(item_id, location, slot) From fc7319564e58a3ece6592e4fe295428630748864 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 9 Jan 2022 16:46:51 -0600 Subject: [PATCH 04/32] properly credit @Black-Sliver for his multi trigger --- WebHostLib/static/assets/tutorial/archipelago/triggers_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/tutorial/archipelago/triggers_en.md b/WebHostLib/static/assets/tutorial/archipelago/triggers_en.md index 46d5cd1f..30ffc2f0 100644 --- a/WebHostLib/static/assets/tutorial/archipelago/triggers_en.md +++ b/WebHostLib/static/assets/tutorial/archipelago/triggers_en.md @@ -132,4 +132,4 @@ For example: difficulty: mystery ``` -In this example if the `pupdunk` option is rolled then the difficulty values will be rolled again using the new options `normal`, `pupdunk_hard`, and `pupdunk_mystery`, and the exp modifier will be rerolled using new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard` and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery". \ No newline at end of file +In this example (thanks to @Black-Sliver) if the `pupdunk` option is rolled then the difficulty values will be rolled again using the new options `normal`, `pupdunk_hard`, and `pupdunk_mystery`, and the exp modifier will be rerolled using new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard` and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery". \ No newline at end of file From faabcd8cb7561f2a2a9d3bea16ec2e6b7bf7893d Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 9 Jan 2022 18:05:57 -0600 Subject: [PATCH 05/32] remove a double paste that somehow showed up? --- .../tutorial/archipelago/triggers_en.md | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/WebHostLib/static/assets/tutorial/archipelago/triggers_en.md b/WebHostLib/static/assets/tutorial/archipelago/triggers_en.md index 30ffc2f0..6fb17ff0 100644 --- a/WebHostLib/static/assets/tutorial/archipelago/triggers_en.md +++ b/WebHostLib/static/assets/tutorial/archipelago/triggers_en.md @@ -66,41 +66,6 @@ In this example if your world happens to roll SpecificKeycards then your game wi It is also possible to use imaginary names in options to trigger specific settings. You can use these made up names in either your main options or to trigger from another trigger. Currently, this is the only way to trigger on "setting 1 AND setting 2". -For example: - ```yaml - triggers: - - option_category: Secret of Evermore - option_name: doggomizer - option_result: pupdunk - options: - Secret of Evermore: - difficulty: - normal: 50 - pupdunk_hard: 25 - pupdunk_mystery: 25 - exp_modifier: - 150: 50 - 200: 50 - - option_category: Secret of Evermore - option_name: difficulty - option_result: pupdunk_hard - options: - Secret of Evermore: - fix_wings_glitch: false - difficulty: hard - - option_category: Secret of Evermore - option_name: difficulty - option_result: pupdunk_mystery - options: - Secret of Evermore: - fix_wings_glitch: false - difficulty: mystery - ``` - -In this example if the `pupdunk` option is rolled then the difficulty values will be rolled again using the new options `normal`, `pupdunk_hard`, and `pupdunk_mystery`, and the exp modifier will be rerolled using new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard` and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery". - -It is also possible to use imaginary names in options to trigger specific settings. You can use these made up names in either your main options or to trigger from another trigger. Currently this is the only way to trigger on "setting 1 AND setting 2". - For example: ```yaml triggers: From 6c3a4b8ffc1c5e2f5d564452f7416de1c6cdd749 Mon Sep 17 00:00:00 2001 From: Grrmo <72080072+Grrmo@users.noreply.github.com> Date: Mon, 10 Jan 2022 22:08:15 +0100 Subject: [PATCH 06/32] Added German translation for Timespinner (#200) * German translation of the setup guide --- .../assets/tutorial/timespinner/setup_de.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 WebHostLib/static/assets/tutorial/timespinner/setup_de.md diff --git a/WebHostLib/static/assets/tutorial/timespinner/setup_de.md b/WebHostLib/static/assets/tutorial/timespinner/setup_de.md new file mode 100644 index 00000000..6da550b3 --- /dev/null +++ b/WebHostLib/static/assets/tutorial/timespinner/setup_de.md @@ -0,0 +1,41 @@ +# Timespinner Randomizer Installationsanweisungen + +## Benötigte Software + +- [Timespinner (Steam)](https://store.steampowered.com/app/368620/Timespinner/), [Timespinner (Humble)](https://www.humblebundle.com/store/timespinner) oder [Timespinner (GOG)](https://www.gog.com/game/timespinner) (andere Versionen werden nicht unterstützt) +- [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer) + +## Wie funktioniert's? + +Der Timespinner Randomizer lädt die Timespinner.exe im gleichen Verzeichnis und verändert seine Speicherinformationen um die Randomisierung der Gegenstände zu erlauben + +## Installationsanweisungen + +1. Die aktuellsten Dateien des Randomizers findest du ganz oben auf dieser Webseite: [Timespinner Randomizer Releases](https://github.com/JarnoWesthof/TsRandomizer/releases). Lade dir unter 'Assets' die .zip Datei für dein Betriebssystem herunter +2. Entpacke die .zip Datei im Ordner, in dem das Spiel Timespinner installiert ist + +## Den Randomizer starten + +- auf Windows: Starte die Datei TsRandomizer.exe +- auf Linux: Starte die Datei TsRandomizer.bin.x86_64 +- auf Mac: Starte die Datei TsRandomizer.bin.osx + +... im Ordner in dem die Inhalte aus der .zip Datei entpackt wurden + +Weitere Informationen zum Randomizer findest du hier: [ReadMe](https://github.com/JarnoWesthof/TsRandomizer) + +## An einer Multiworld teilnehmen + +1. Starte den Randomizer wie unter [Den Randomizer starten](#Den-Randomizer-starten) beschrieben +2. Wähle "New Game" +3. Wechsle "<< Select Seed >>" zu "<< Archiplago >>" indem du "links" auf deinem Controller oder der Tastatur drückst +4. Wähle "<< Archipelago >>" um ein neues Menu zu öffnen, wo du deine Logininformationen für Archipelago eingeben kannst + * ANMERKUNG: Die Eingabefelder unterstützen das Einfügen von Informationen mittels 'Ctrl + V' +5. Wähle "Connect" +6. Wenn alles funktioniert hat, wechselt das Spiel zurück zur Auswahl des Schwierigkeitsgrads und das Spiel startet, sobald du einen davon ausgewählt hast + +## Woher bekomme ich eine Konfigurationsdatei? +Die [Player Settings](https://archipelago.gg/games/Timespinner/player-settings) Seite auf der Website erlaubt dir, persönliche Einstellungen zu definieren und diese in eine Konfigurationsdatei zu exportieren + +* Die Timespinner Randomizer Option "StinkyMaw" ist in Archipelago Seeds aktuell immer an +* Die Timespinner Randomizer Optionen "ProgressiveVerticalMovement" & "ProgressiveKeycards" werden in Archipelago Seeds aktuell nicht unterstützt From 9be4a91028f977488d17e1530fa0bd9de1b4a384 Mon Sep 17 00:00:00 2001 From: Grrmo <72080072+Grrmo@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:52:31 +0100 Subject: [PATCH 07/32] Added German Tutorial for Timespinner --- WebHostLib/static/assets/tutorial/tutorials.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/WebHostLib/static/assets/tutorial/tutorials.json b/WebHostLib/static/assets/tutorial/tutorials.json index 67aa2086..fa477510 100644 --- a/WebHostLib/static/assets/tutorial/tutorials.json +++ b/WebHostLib/static/assets/tutorial/tutorials.json @@ -262,6 +262,16 @@ "authors": [ "Jarno" ] + }, + { + "language": "German", + "filename": "timespinner/setup_de.md", + "link": "timespinner/setup/de", + "authors": [ + "Grrmo", + "Fynxes", + "Blaze0168" + ] } ] } From d1146b4fbc4be96fca7c1035b9b7443cfe19eab2 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Mon, 10 Jan 2022 22:08:06 -0500 Subject: [PATCH 08/32] Add weighted-settings link to player-settings --- WebHostLib/templates/player-settings.html | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/WebHostLib/templates/player-settings.html b/WebHostLib/templates/player-settings.html index 18dc9032..6c169859 100644 --- a/WebHostLib/templates/player-settings.html +++ b/WebHostLib/templates/player-settings.html @@ -18,10 +18,13 @@ or download a settings file you can use to participate in a MultiWorld.

- A list of all games you have generated can be found here. + A more advanced settings configuration for all games can be found on the + Player Settings page.
- Advanced users can download a template file for this game - here. + A list of all games you have generated can be found on the User Content Page. +
+ You may also download the + template file for this game.

A more advanced settings configuration for all games can be found on the - Player Settings page. + Weighted Settings page.
A list of all games you have generated can be found on the User Content Page.
From fe25c9c4833e8ef6d992e3138929fdcb6e4bf9e3 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Mon, 10 Jan 2022 23:20:15 -0500 Subject: [PATCH 10/32] Improve styling on weighted-settings --- WebHostLib/static/assets/weighted-settings.js | 27 +++++++++++++++++-- .../static/styles/weighted-settings.css | 11 +++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index 7d0a9698..1471fc41 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -95,14 +95,13 @@ const createDefaultSettings = (settingData) => { newSettings[game].local_items = []; newSettings[game].non_local_items = []; newSettings[game].start_hints = []; + newSettings[game].start_location_hints = []; } localStorage.setItem('weighted-settings', JSON.stringify(newSettings)); } }; -// TODO: Include item configs: start_inventory, local_items, non_local_items, start_hints -// TODO: Include location configs: exclude_locations const buildUI = (settingData) => { // Build the game-choice div buildGameChoice(settingData.games); @@ -133,12 +132,17 @@ const buildUI = (settingData) => { const itemsDiv = buildItemsDiv(game, settingData.games[game].gameItems); gameDiv.appendChild(itemsDiv); + + const hintsDiv = buildHintsDiv(game, settingData.games[game].gameItems, settingData.games[game].gameLocations); + gameDiv.appendChild(hintsDiv); + gamesWrapper.appendChild(gameDiv); collapseButton.addEventListener('click', () => { collapseButton.classList.add('invisible'); weightedSettingsDiv.classList.add('invisible'); itemsDiv.classList.add('invisible'); + hintsDiv.classList.add('invisible'); expandButton.classList.remove('invisible'); }); @@ -146,6 +150,7 @@ const buildUI = (settingData) => { collapseButton.classList.remove('invisible'); weightedSettingsDiv.classList.remove('invisible'); itemsDiv.classList.remove('invisible'); + hintsDiv.classList.remove('invisible'); expandButton.classList.add('invisible'); }); }); @@ -621,6 +626,24 @@ const itemDropHandler = (evt) => { localStorage.setItem('weighted-settings', JSON.stringify(currentSettings)); }; +const buildHintsDiv = (game, items, locations) => { + const hintsDiv = document.createElement('div'); + hintsDiv.classList.add('hints-div'); + const hintsHeader = document.createElement('h3'); + hintsHeader.innerText = 'Item & Location Hints'; + hintsDiv.appendChild(hintsHeader); + const hintsDescription = document.createElement('p'); + hintsDescription.classList.add('setting-description'); + hintsDescription.innerText = 'Choose any items or locations to begin the game with the knowledge of where those ' + + ' items, are or what those locations contain. Excluded locations will not contain progression items.'; + hintsDiv.appendChild(hintsDescription); + + const itemHintsDiv = document.createElement('div'); + + hintsDiv.appendChild(itemHintsDiv); + return hintsDiv; +}; + const updateVisibleGames = () => { const settings = JSON.parse(localStorage.getItem('weighted-settings')); Object.keys(settings.game).forEach((game) => { diff --git a/WebHostLib/static/styles/weighted-settings.css b/WebHostLib/static/styles/weighted-settings.css index 28ab4a10..eca2b7cc 100644 --- a/WebHostLib/static/styles/weighted-settings.css +++ b/WebHostLib/static/styles/weighted-settings.css @@ -111,10 +111,11 @@ html{ height: 300px; overflow-y: auto; overflow-x: hidden; + margin-top: 0.25rem; } #weighted-settings .items-wrapper .item-container .item-div{ - padding: 0.15rem; + padding: 0.125rem 0.5rem; cursor: pointer; } @@ -122,6 +123,14 @@ html{ background-color: rgba(0, 0, 0, 0.1); } +#weighted-settings .hints-div{ + margin-top: 2rem; +} + +#weighted-settings .hints-div h3{ + margin-bottom: 0.5rem; +} + #weighted-settings #weighted-settings-button-row{ display: flex; flex-direction: row; From c330f4a35e48f6d693ff67115cc695b78f1c580f Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Tue, 11 Jan 2022 01:26:12 -0500 Subject: [PATCH 11/32] [WebHost] weighted-settings: Implement item and location hints --- WebHostLib/static/assets/weighted-settings.js | 144 +++++++++++++++++- .../static/styles/weighted-settings.css | 37 ++++- 2 files changed, 173 insertions(+), 8 deletions(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index 1471fc41..893af20a 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -479,6 +479,9 @@ const buildWeightedSettingsDiv = (game, settings) => { }; const buildItemsDiv = (game, items) => { + // Sort alphabetical, in pace + items.sort(); + const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); const itemsDiv = document.createElement('div'); itemsDiv.classList.add('items-div'); @@ -542,7 +545,7 @@ const buildItemsDiv = (game, items) => { nonLocalItems.addEventListener('drop', itemDropHandler); // Populate the divs - items.sort().forEach((item) => { + items.forEach((item) => { const itemDiv = buildItemDiv(game, item); if (currentSettings[game].start_inventory.includes(item)){ @@ -627,6 +630,12 @@ const itemDropHandler = (evt) => { }; const buildHintsDiv = (game, items, locations) => { + const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); + + // Sort alphabetical, in place + items.sort(); + locations.sort(); + const hintsDiv = document.createElement('div'); hintsDiv.classList.add('hints-div'); const hintsHeader = document.createElement('h3'); @@ -635,15 +644,142 @@ const buildHintsDiv = (game, items, locations) => { const hintsDescription = document.createElement('p'); hintsDescription.classList.add('setting-description'); hintsDescription.innerText = 'Choose any items or locations to begin the game with the knowledge of where those ' + - ' items, are or what those locations contain. Excluded locations will not contain progression items.'; + ' items are, or what those locations contain. Excluded locations will not contain progression items.'; hintsDiv.appendChild(hintsDescription); - const itemHintsDiv = document.createElement('div'); + const itemHintsContainer = document.createElement('div'); + itemHintsContainer.classList.add('hints-container'); - hintsDiv.appendChild(itemHintsDiv); + const itemHintsWrapper = document.createElement('div'); + itemHintsWrapper.classList.add('hints-wrapper'); + itemHintsWrapper.innerText = 'Starting Item Hints'; + + const itemHintsDiv = document.createElement('div'); + itemHintsDiv.classList.add('item-container'); + items.forEach((item) => { + const itemDiv = document.createElement('div'); + itemDiv.classList.add('hint-div'); + + const itemLabel = document.createElement('label'); + itemLabel.setAttribute('for', `${game}-start_hints-${item}`); + + const itemCheckbox = document.createElement('input'); + itemCheckbox.setAttribute('type', 'checkbox'); + itemCheckbox.setAttribute('id', `${game}-start_hints-${item}`); + itemCheckbox.setAttribute('data-game', game); + itemCheckbox.setAttribute('data-setting', 'start_hints'); + itemCheckbox.setAttribute('data-option', item); + if (currentSettings[game].start_hints.includes(item)) { + itemCheckbox.setAttribute('checked', 'true'); + } + itemCheckbox.addEventListener('change', hintChangeHandler); + itemLabel.appendChild(itemCheckbox); + + const itemName = document.createElement('span'); + itemName.innerText = item; + itemLabel.appendChild(itemName); + + itemDiv.appendChild(itemLabel); + itemHintsDiv.appendChild(itemDiv); + }); + + itemHintsWrapper.appendChild(itemHintsDiv); + itemHintsContainer.appendChild(itemHintsWrapper); + + const locationHintsWrapper = document.createElement('div'); + locationHintsWrapper.classList.add('hints-wrapper'); + locationHintsWrapper.innerText = 'Starting Location Hints'; + + const locationHintsDiv = document.createElement('div'); + locationHintsDiv.classList.add('item-container'); + locations.forEach((location) => { + const locationDiv = document.createElement('div'); + locationDiv.classList.add('hint-div'); + + const locationLabel = document.createElement('label'); + locationLabel.setAttribute('for', `${game}-start_location_hints-${location}`); + + const locationCheckbox = document.createElement('input'); + locationCheckbox.setAttribute('type', 'checkbox'); + locationCheckbox.setAttribute('id', `${game}-start_location_hints-${location}`); + locationCheckbox.setAttribute('data-game', game); + locationCheckbox.setAttribute('data-setting', 'start_location_hints'); + locationCheckbox.setAttribute('data-option', location); + if (currentSettings[game].start_location_hints.includes(location)) { + locationCheckbox.setAttribute('checked', '1'); + } + locationCheckbox.addEventListener('change', hintChangeHandler); + locationLabel.appendChild(locationCheckbox); + + const locationName = document.createElement('span'); + locationName.innerText = location; + locationLabel.appendChild(locationName); + + locationDiv.appendChild(locationLabel); + locationHintsDiv.appendChild(locationDiv); + }); + + locationHintsWrapper.appendChild(locationHintsDiv); + itemHintsContainer.appendChild(locationHintsWrapper); + + const excludeLocationsWrapper = document.createElement('div'); + excludeLocationsWrapper.classList.add('hints-wrapper'); + excludeLocationsWrapper.innerText = 'Exclude Locations'; + + const excludeLocationsDiv = document.createElement('div'); + excludeLocationsDiv.classList.add('item-container'); + locations.forEach((location) => { + const locationDiv = document.createElement('div'); + locationDiv.classList.add('hint-div'); + + const locationLabel = document.createElement('label'); + locationLabel.setAttribute('for', `${game}-exclude_locations-${location}`); + + const locationCheckbox = document.createElement('input'); + locationCheckbox.setAttribute('type', 'checkbox'); + locationCheckbox.setAttribute('id', `${game}-exclude_locations-${location}`); + locationCheckbox.setAttribute('data-game', game); + locationCheckbox.setAttribute('data-setting', 'exclude_locations'); + locationCheckbox.setAttribute('data-option', location); + if (currentSettings[game].exclude_locations.includes(location)) { + locationCheckbox.setAttribute('checked', '1'); + } + locationCheckbox.addEventListener('change', hintChangeHandler); + locationLabel.appendChild(locationCheckbox); + + const locationName = document.createElement('span'); + locationName.innerText = location; + locationLabel.appendChild(locationName); + + locationDiv.appendChild(locationLabel); + excludeLocationsDiv.appendChild(locationDiv); + }); + + excludeLocationsWrapper.appendChild(excludeLocationsDiv); + itemHintsContainer.appendChild(excludeLocationsWrapper); + + hintsDiv.appendChild(itemHintsContainer); return hintsDiv; }; +const hintChangeHandler = (evt) => { + const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); + const game = evt.target.getAttribute('data-game'); + const setting = evt.target.getAttribute('data-setting'); + const option = evt.target.getAttribute('data-option'); + + if (evt.target.checked) { + if (!currentSettings[game][setting].includes(option)) { + currentSettings[game][setting].push(option); + } + } else { + if (currentSettings[game][setting].includes(option)) { + currentSettings[game][setting].splice(currentSettings[game][setting].indexOf(option), 1); + } + } + localStorage.setItem('weighted-settings', JSON.stringify(currentSettings)); +}; + const updateVisibleGames = () => { const settings = JSON.parse(localStorage.getItem('weighted-settings')); Object.keys(settings.game).forEach((game) => { diff --git a/WebHostLib/static/styles/weighted-settings.css b/WebHostLib/static/styles/weighted-settings.css index eca2b7cc..a78fb2e7 100644 --- a/WebHostLib/static/styles/weighted-settings.css +++ b/WebHostLib/static/styles/weighted-settings.css @@ -102,24 +102,26 @@ html{ #weighted-settings .items-wrapper .item-set-wrapper{ width: 24%; + font-weight: bold; } -#weighted-settings .items-wrapper .item-container{ +#weighted-settings .item-container{ border: 1px solid #ffffff; border-radius: 2px; width: 100%; height: 300px; overflow-y: auto; overflow-x: hidden; - margin-top: 0.25rem; + margin-top: 0.125rem; + font-weight: normal; } -#weighted-settings .items-wrapper .item-container .item-div{ +#weighted-settings .item-container .item-div{ padding: 0.125rem 0.5rem; cursor: pointer; } -#weighted-settings .items-wrapper .item-container .item-div:hover{ +#weighted-settings .item-container .item-div:hover{ background-color: rgba(0, 0, 0, 0.1); } @@ -131,6 +133,33 @@ html{ margin-bottom: 0.5rem; } +#weighted-settings .hints-div .hints-container{ + display: flex; + flex-direction: row; + justify-content: space-between; + font-weight: bold; +} + +#weighted-settings .hints-div .hints-wrapper{ + width: 32.5%; +} + +#weighted-settings .hints-div .hints-wrapper .hint-div{ + display: flex; + flex-direction: row; + cursor: pointer; +} + +#weighted-settings .hints-div .hints-wrapper .hint-div:hover{ + background-color: rgba(0, 0, 0, 0.1); +} + +#weighted-settings .hints-div .hints-wrapper .hint-div label{ + flex-grow: 1; + padding: 0.125rem 0.5rem; + cursor: pointer; +} + #weighted-settings #weighted-settings-button-row{ display: flex; flex-direction: row; From f33a15dc4e42130a5d9e0b2148123a7496ddf650 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Tue, 11 Jan 2022 01:34:48 -0500 Subject: [PATCH 12/32] [WebHost] weighted-settings: Added a brief description of what a weighted setting is at the top of the page. --- WebHostLib/templates/weighted-settings.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/WebHostLib/templates/weighted-settings.html b/WebHostLib/templates/weighted-settings.html index 7d60e724..0274d654 100644 --- a/WebHostLib/templates/weighted-settings.html +++ b/WebHostLib/templates/weighted-settings.html @@ -14,6 +14,10 @@

Weighted Settings

+

Weighted Settings allows you to choose how likely a particular option is to be used in game generation. + The higher an option is weighted, the more likely the option will be chosen. Think of them like + entries in a raffle.

+

Choose the games and options you would like to play with! You may generate a single-player game from this page, or download a settings file you can use to participate in a MultiWorld.

From 71c2db0829b7e3f347dd0eefb03b278626122d9d Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Tue, 11 Jan 2022 01:36:06 -0500 Subject: [PATCH 13/32] [WebHost] weighted-settings: Improved link to /user-content --- WebHostLib/templates/weighted-settings.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WebHostLib/templates/weighted-settings.html b/WebHostLib/templates/weighted-settings.html index 0274d654..de842682 100644 --- a/WebHostLib/templates/weighted-settings.html +++ b/WebHostLib/templates/weighted-settings.html @@ -21,7 +21,8 @@

Choose the games and options you would like to play with! You may generate a single-player game from this page, or download a settings file you can use to participate in a MultiWorld.

-

A list of all games you have generated can be found here.

+

A list of all games you have generated can be found on the User Content + page.


From a0ade9ea317333d7d2b9f99f5bc598d50ce6489c Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Tue, 11 Jan 2022 01:56:14 -0500 Subject: [PATCH 14/32] [WebHost] weighted-settings: Added basic validation before export --- WebHostLib/static/assets/weighted-settings.js | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index 893af20a..9a298ce3 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -837,11 +837,17 @@ const updateGameSetting = (event) => { const exportSettings = () => { const settings = JSON.parse(localStorage.getItem('weighted-settings')); + const userMessage = document.getElementById('user-message'); + let errorMessage = null; + + // User must choose a name for their file if (!settings.name || settings.name.trim().length === 0 || settings.name.toLowerCase().trim() === 'player') { - const userMessage = document.getElementById('user-message'); userMessage.innerText = 'You forgot to set your player name at the top of the page!'; userMessage.classList.add('visible'); - window.scrollTo(0, 0); + userMessage.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); return; } @@ -854,6 +860,10 @@ const exportSettings = () => { return; } + if (Object.keys(settings.game).length === 0) { + errorMessage = 'You have not chosen a game to play!'; + } + // Remove any disabled options Object.keys(settings[game]).forEach((setting) => { Object.keys(settings[game][setting]).forEach((option) => { @@ -861,9 +871,27 @@ const exportSettings = () => { delete settings[game][setting][option]; } }); + + if (Object.keys(settings[game][setting]).length === 0 && !Array.isArray(settings[game][setting])) { + errorMessage = `${game} // ${setting} has no values above zero!`; + } }); }); + // If an error occurred, alert the user and do not export the file + if (errorMessage) { + userMessage.innerText = errorMessage; + userMessage.classList.add('visible'); + userMessage.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + return; + } + + // If no error occurred, hide the user message if it is visible + userMessage.classList.remove('visible'); + const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`); download(`${document.getElementById('player-name').value}.yaml`, yamlText); }; @@ -894,7 +922,10 @@ const generateGame = (raceMode = false) => { userMessage.innerText += ' ' + error.response.data.text; } userMessage.classList.add('visible'); - window.scrollTo(0, 0); + userMessage.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); console.error(error); }); }; From 9f5a2d1eb3dbf4a5c4faf1906e794db4d10042ce Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Tue, 11 Jan 2022 02:01:31 -0500 Subject: [PATCH 15/32] [WebHost] weighted-settings: Validate settings before allowing game generation or export --- WebHostLib/static/assets/weighted-settings.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index 9a298ce3..a89a003f 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -835,7 +835,7 @@ const updateGameSetting = (event) => { localStorage.setItem('weighted-settings', JSON.stringify(options)); }; -const exportSettings = () => { +const validateSettings = () => { const settings = JSON.parse(localStorage.getItem('weighted-settings')); const userMessage = document.getElementById('user-message'); let errorMessage = null; @@ -891,6 +891,12 @@ const exportSettings = () => { // If no error occurred, hide the user message if it is visible userMessage.classList.remove('visible'); + return settings; +}; + +const exportSettings = () => { + const settings = validateSettings(); + if (!settings) { return; } const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`); download(`${document.getElementById('player-name').value}.yaml`, yamlText); @@ -908,9 +914,12 @@ const download = (filename, text) => { }; const generateGame = (raceMode = false) => { + const settings = validateSettings(); + if (!settings) { return; } + axios.post('/api/generate', { - weights: { player: localStorage.getItem('weighted-settings') }, - presetData: { player: localStorage.getItem('weighted-settings') }, + weights: { player: JSON.stringify(settings) }, + presetData: { player: JSON.stringify(settings) }, playerCount: 1, race: raceMode ? '1' : '0', }).then((response) => { From 9339019308cd603b47e2802ae307ff87bc24af4b Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Tue, 11 Jan 2022 02:26:11 -0500 Subject: [PATCH 16/32] [WebHost] weighted-settings: Fix a bug in game choice validation --- WebHostLib/static/assets/weighted-settings.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index a89a003f..15a6f1db 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -860,10 +860,6 @@ const validateSettings = () => { return; } - if (Object.keys(settings.game).length === 0) { - errorMessage = 'You have not chosen a game to play!'; - } - // Remove any disabled options Object.keys(settings[game]).forEach((setting) => { Object.keys(settings[game][setting]).forEach((option) => { @@ -878,6 +874,10 @@ const validateSettings = () => { }); }); + if (Object.keys(settings.game).length === 0) { + errorMessage = 'You have not chosen a game to play!'; + } + // If an error occurred, alert the user and do not export the file if (errorMessage) { userMessage.innerText = errorMessage; From 240d1423a386d9bb759d73e73edc4de5ae038f14 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Tue, 11 Jan 2022 04:20:33 -0500 Subject: [PATCH 17/32] [WebHost] weighted-settings: Fix start_inventory using the wrong data type --- WebHostLib/static/assets/weighted-settings.js | 95 +++++++++++++++---- .../static/styles/weighted-settings.css | 18 ++++ 2 files changed, 95 insertions(+), 18 deletions(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index 15a6f1db..36df7d3f 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -90,7 +90,7 @@ const createDefaultSettings = (settingData) => { } } - newSettings[game].start_inventory = []; + newSettings[game].start_inventory = {}; newSettings[game].exclude_locations = []; newSettings[game].local_items = []; newSettings[game].non_local_items = []; @@ -546,18 +546,20 @@ const buildItemsDiv = (game, items) => { // Populate the divs items.forEach((item) => { - const itemDiv = buildItemDiv(game, item); - - if (currentSettings[game].start_inventory.includes(item)){ + if (Object.keys(currentSettings[game].start_inventory).includes(item)){ + const itemDiv = buildItemQtyDiv(game, item); itemDiv.setAttribute('data-setting', 'start_inventory'); startInventory.appendChild(itemDiv); } else if (currentSettings[game].local_items.includes(item)) { + const itemDiv = buildItemDiv(game, item); itemDiv.setAttribute('data-setting', 'local_items'); localItems.appendChild(itemDiv); } else if (currentSettings[game].non_local_items.includes(item)) { + const itemDiv = buildItemDiv(game, item); itemDiv.setAttribute('data-setting', 'non_local_items'); nonLocalItems.appendChild(itemDiv); } else { + const itemDiv = buildItemDiv(game, item); availableItems.appendChild(itemDiv); } }); @@ -588,6 +590,35 @@ const buildItemDiv = (game, item) => { return itemDiv; }; +const buildItemQtyDiv = (game, item) => { + const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); + const itemQtyDiv = document.createElement('div'); + itemQtyDiv.classList.add('item-qty-div'); + itemQtyDiv.setAttribute('id', `${game}-${item}`); + itemQtyDiv.setAttribute('data-game', game); + itemQtyDiv.setAttribute('data-item', item); + itemQtyDiv.setAttribute('draggable', 'true'); + itemQtyDiv.innerText = item; + + const itemQty = document.createElement('input'); + itemQty.setAttribute('value', currentSettings[game].start_inventory.hasOwnProperty(item) ? + currentSettings[game].start_inventory[item] : '1'); + itemQty.setAttribute('data-game', game); + itemQty.setAttribute('data-setting', 'start_inventory'); + itemQty.setAttribute('data-option', item); + itemQty.setAttribute('maxlength', '3'); + itemQty.addEventListener('keyup', (evt) => { + evt.target.value = isNaN(parseInt(evt.target.value)) ? 0 : parseInt(evt.target.value); + updateItemSetting(evt); + }); + itemQtyDiv.appendChild(itemQty); + + itemQtyDiv.addEventListener('dragstart', (evt) => { + evt.dataTransfer.setData('text/plain', itemQtyDiv.getAttribute('id')); + }); + return itemQtyDiv; +}; + const itemDragoverHandler = (evt) => { evt.preventDefault(); }; @@ -600,22 +631,33 @@ const itemDropHandler = (evt) => { const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); const game = sourceDiv.getAttribute('data-game'); const item = sourceDiv.getAttribute('data-item'); - const itemDiv = buildItemDiv(game, item); const oldSetting = sourceDiv.hasAttribute('data-setting') ? sourceDiv.getAttribute('data-setting') : null; const newSetting = evt.target.hasAttribute('data-setting') ? evt.target.getAttribute('data-setting') : null; + const itemDiv = newSetting === 'start_inventory' ? buildItemQtyDiv(game, item) : buildItemDiv(game, item); + if (oldSetting) { - if (currentSettings[game][oldSetting].includes(item)) { - currentSettings[game][oldSetting].splice(currentSettings[game][oldSetting].indexOf(item), 1); + if (oldSetting === 'start_inventory') { + if (currentSettings[game][oldSetting].hasOwnProperty(item)) { + delete currentSettings[game][oldSetting][item]; + } + } else { + if (currentSettings[game][oldSetting].includes(item)) { + currentSettings[game][oldSetting].splice(currentSettings[game][oldSetting].indexOf(item), 1); + } } } if (newSetting) { itemDiv.setAttribute('data-setting', newSetting); document.getElementById(`${game}-${newSetting}`).appendChild(itemDiv); - if (!currentSettings[game][newSetting].includes(item)){ - currentSettings[game][newSetting].push(item); + if (newSetting === 'start_inventory') { + currentSettings[game][newSetting][item] = 1; + } else { + if (!currentSettings[game][newSetting].includes(item)){ + currentSettings[game][newSetting].push(item); + } } } else { // No setting was assigned, this item has been removed from the settings @@ -823,15 +865,28 @@ const updateBaseSetting = (event) => { localStorage.setItem('weighted-settings', JSON.stringify(settings)); }; -const updateGameSetting = (event) => { +const updateGameSetting = (evt) => { const options = JSON.parse(localStorage.getItem('weighted-settings')); - const game = event.target.getAttribute('data-game'); - const setting = event.target.getAttribute('data-setting'); - const option = event.target.getAttribute('data-option'); - const type = event.target.getAttribute('data-type'); - document.getElementById(`${game}-${setting}-${option}`).innerText = event.target.value; - options[game][setting][option] = isNaN(event.target.value) ? - event.target.value : parseInt(event.target.value, 10); + const game = evt.target.getAttribute('data-game'); + const setting = evt.target.getAttribute('data-setting'); + const option = evt.target.getAttribute('data-option'); + document.getElementById(`${game}-${setting}-${option}`).innerText = evt.target.value; + options[game][setting][option] = isNaN(evt.target.value) ? + evt.target.value : parseInt(evt.target.value, 10); + localStorage.setItem('weighted-settings', JSON.stringify(options)); +}; + +const updateItemSetting = (evt) => { + const options = JSON.parse(localStorage.getItem('weighted-settings')); + const game = evt.target.getAttribute('data-game'); + const setting = evt.target.getAttribute('data-setting'); + const option = evt.target.getAttribute('data-option'); + if (setting === 'start_inventory') { + options[game][setting][option] = evt.target.value.trim() ? parseInt(evt.target.value) : 0; + } else { + options[game][setting][option] = isNaN(evt.target.value) ? + evt.target.value : parseInt(evt.target.value, 10); + } localStorage.setItem('weighted-settings', JSON.stringify(options)); }; @@ -868,7 +923,11 @@ const validateSettings = () => { } }); - if (Object.keys(settings[game][setting]).length === 0 && !Array.isArray(settings[game][setting])) { + if ( + Object.keys(settings[game][setting]).length === 0 && + !Array.isArray(settings[game][setting]) && + setting !== 'start_inventory' + ) { errorMessage = `${game} // ${setting} has no values above zero!`; } }); diff --git a/WebHostLib/static/styles/weighted-settings.css b/WebHostLib/static/styles/weighted-settings.css index a78fb2e7..5311f74a 100644 --- a/WebHostLib/static/styles/weighted-settings.css +++ b/WebHostLib/static/styles/weighted-settings.css @@ -125,6 +125,24 @@ html{ background-color: rgba(0, 0, 0, 0.1); } +#weighted-settings .item-container .item-qty-div{ + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 0.125rem 0.5rem; + cursor: pointer; +} + +#weighted-settings .item-container .item-qty-div input{ + min-width: unset; + width: 1.5rem; + text-align: center; +} + +#weighted-settings .item-container .item-qty-div:hover{ + background-color: rgba(0, 0, 0, 0.1); +} + #weighted-settings .hints-div{ margin-top: 2rem; } From ee190601ee0ff0fe12a7a98920591ed2af6c5600 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Tue, 11 Jan 2022 04:33:27 -0500 Subject: [PATCH 18/32] [WebHost] weighted-settings: Minor style fixes --- WebHostLib/static/assets/weighted-settings.js | 6 +++++- WebHostLib/static/styles/weighted-settings.css | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index 36df7d3f..df0912fd 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -600,6 +600,9 @@ const buildItemQtyDiv = (game, item) => { itemQtyDiv.setAttribute('draggable', 'true'); itemQtyDiv.innerText = item; + const inputWrapper = document.createElement('div'); + inputWrapper.classList.add('item-qty-input-wrapper') + const itemQty = document.createElement('input'); itemQty.setAttribute('value', currentSettings[game].start_inventory.hasOwnProperty(item) ? currentSettings[game].start_inventory[item] : '1'); @@ -611,7 +614,8 @@ const buildItemQtyDiv = (game, item) => { evt.target.value = isNaN(parseInt(evt.target.value)) ? 0 : parseInt(evt.target.value); updateItemSetting(evt); }); - itemQtyDiv.appendChild(itemQty); + inputWrapper.appendChild(itemQty); + itemQtyDiv.appendChild(inputWrapper); itemQtyDiv.addEventListener('dragstart', (evt) => { evt.dataTransfer.setData('text/plain', itemQtyDiv.getAttribute('id')); diff --git a/WebHostLib/static/styles/weighted-settings.css b/WebHostLib/static/styles/weighted-settings.css index 5311f74a..6c2a46ff 100644 --- a/WebHostLib/static/styles/weighted-settings.css +++ b/WebHostLib/static/styles/weighted-settings.css @@ -133,6 +133,12 @@ html{ cursor: pointer; } +#weighted-settings .item-container .item-qty-div .item-qty-input-wrapper{ + display: flex; + flex-direction: column; + justify-content: space-around; +} + #weighted-settings .item-container .item-qty-div input{ min-width: unset; width: 1.5rem; @@ -166,6 +172,8 @@ html{ display: flex; flex-direction: row; cursor: pointer; + user-select: none; + -moz-user-select: none; } #weighted-settings .hints-div .hints-wrapper .hint-div:hover{ From 3acd966241b2fb46b5d0fcc46b44d16ba00a4e1c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 11 Jan 2022 22:01:54 +0100 Subject: [PATCH 19/32] Options: add "VerifyKeys" Mixin and showcase it for OoT Logic Tricks --- Options.py | 20 ++++++++++++++++++-- WebHostLib/options.py | 8 ++++++++ worlds/oot/Options.py | 5 ++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Options.py b/Options.py index a5ef5f9c..637f28c6 100644 --- a/Options.py +++ b/Options.py @@ -244,7 +244,21 @@ class OptionNameSet(Option): return cls.from_text(str(data)) -class OptionDict(Option): +class VerifyKeys: + valid_keys = frozenset() + valid_keys_casefold: bool = False + + @classmethod + def verify_keys(cls, data): + if cls.valid_keys: + dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data) + extra = dataset - cls.valid_keys + if extra: + raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. " + f"Allowed keys: {cls.valid_keys}.") + + +class OptionDict(Option, VerifyKeys): default = {} supports_weighting = False value: typing.Dict[str, typing.Any] @@ -255,6 +269,7 @@ class OptionDict(Option): @classmethod def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict: if type(data) == dict: + cls.verify_keys(set(data)) return cls(data) else: raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") @@ -276,7 +291,7 @@ class ItemDict(OptionDict): super(ItemDict, self).__init__(value) -class OptionList(Option): +class OptionList(Option, VerifyKeys): default = [] supports_weighting = False value: list @@ -292,6 +307,7 @@ class OptionList(Option): @classmethod def from_any(cls, data: typing.Any): if type(data) == list: + cls.verify_keys(set(data)) return cls(data) return cls.from_text(str(data)) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 8dff42cd..5866b638 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -1,3 +1,4 @@ +import logging import os from Utils import __version__ from jinja2 import Template @@ -9,6 +10,9 @@ import Options target_folder = os.path.join("WebHostLib", "static", "generated") +handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints", + "exclude_locations"} + def create(): os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True) @@ -91,6 +95,10 @@ def create(): "min": option.range_start, "max": option.range_end, } + elif option_name in handled_in_js: + pass + else: + logging.debug(f"{option} not exported to Web Settings.") player_settings["gameOptions"] = game_options diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index e46927a3..d7383e08 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -1,5 +1,6 @@ import typing -from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionList, DeathLink +from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, DeathLink +from .LogicTricks import normalized_name_tricks from .ColorSFXOptions import * @@ -824,6 +825,8 @@ class LogicTricks(OptionList): Format as a comma-separated list of "nice" names: ["Fewer Tunic Requirements", "Hidden Grottos without Stone of Agony"]. A full list of supported tricks can be found at https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/LogicTricks.py""" displayname = "Logic Tricks" + valid_keys = frozenset(normalized_name_tricks) + valid_keys_casefold = True # All options assembled into a single dict From 4e674e0380ec24c1a9ee52c476930861b0861b0e Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Tue, 11 Jan 2022 17:36:33 -0500 Subject: [PATCH 20/32] [WebHost] weighted-settings: Add items-list, locations-list, and custom-list to JSON config file --- WebHostLib/options.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 5866b638..55a61fa8 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -63,7 +63,10 @@ def create(): game_options = {} for option_name, option in all_options.items(): - if option.options: + if option_name in handled_in_js: + pass + + elif option.options: game_options[option_name] = this_option = { "type": "select", "displayName": option.displayname if hasattr(option, "displayname") else option_name, @@ -95,8 +98,30 @@ def create(): "min": option.range_start, "max": option.range_end, } - elif option_name in handled_in_js: - pass + + elif getattr(option, "verify_item_name", False): + game_options[option_name] = { + "type": "items-list", + "displayName": option.displayname if hasattr(option, "displayname") else option_name, + "description": option.__doc__ if option.__doc__ else "Please document me!", + } + + elif getattr(option, "verify_location_name", False): + game_options[option_name] = { + "type": "locations-list", + "displayName": option.displayname if hasattr(option, "displayname") else option_name, + "description": option.__doc__ if option.__doc__ else "Please document me!", + } + + elif hasattr(option, "valid_keys"): + if option.valid_keys: + game_options[option_name] = { + "type": "custom-list", + "displayName": option.displayname if hasattr(option, "displayname") else option_name, + "description": option.__doc__ if option.__doc__ else "Please document me!", + "options": list(option.valid_keys), + } + else: logging.debug(f"{option} not exported to Web Settings.") From 01d6735803dc120fad036dcb745863fbf5a8e47b Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Tue, 11 Jan 2022 18:00:03 -0500 Subject: [PATCH 21/32] [WebHost] weighted-settings: Accept new options in switch for option type --- WebHostLib/static/assets/weighted-settings.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index df0912fd..ed35eff6 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -467,6 +467,18 @@ const buildWeightedSettingsDiv = (game, settings) => { settingWrapper.appendChild(rangeTable); break; + case 'items-list': + // TODO + break; + + case 'locations-list': + // TODO + break; + + case 'custom-list': + // TODO + break; + default: console.error(`Unknown setting type for ${game} setting ${setting}: ${settings[setting].type}`); return; From 684bb736bce2961d9d8949a3cee015d44876c72d Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Tue, 11 Jan 2022 18:06:22 -0500 Subject: [PATCH 22/32] [WebHost] weighted-settings: Include new option types when creating the default settings --- WebHostLib/static/assets/weighted-settings.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index ed35eff6..b33d7990 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -85,6 +85,13 @@ const createDefaultSettings = (settingData) => { newSettings[game][gameSetting]['random-low'] = 0; newSettings[game][gameSetting]['random-high'] = 0; break; + + case 'items-list': + case 'locations-list': + case 'custom-list': + newSettings[game][gameSetting] = []; + break; + default: console.error(`Unknown setting type for ${game} setting ${gameSetting}: ${setting.type}`); } From 1990b893e5d69ff66bde5e51e5e6d85eebc99c25 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 13 Jan 2022 07:40:26 +0100 Subject: [PATCH 23/32] WebHost: fix /api/get_rooms and /api/get_seeds --- WebHostLib/api/generate.py | 1 - WebHostLib/api/user.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/WebHostLib/api/generate.py b/WebHostLib/api/generate.py index 2868a079..90280eba 100644 --- a/WebHostLib/api/generate.py +++ b/WebHostLib/api/generate.py @@ -65,7 +65,6 @@ def generate_api(): return {"text": "Uncaught Exception:" + str(e)}, 500 - @api_endpoints.route('/status/') def wait_seed_api(seed: UUID): seed_id = seed diff --git a/WebHostLib/api/user.py b/WebHostLib/api/user.py index 43a7bdcf..b68f80bb 100644 --- a/WebHostLib/api/user.py +++ b/WebHostLib/api/user.py @@ -16,7 +16,6 @@ def get_rooms(): "last_port": room.last_port, "timeout": room.timeout, "tracker": room.tracker, - "players": room.seed.multidata["names"] if room.seed.multidata else [["Singleplayer"]], }) return jsonify(response) @@ -28,6 +27,6 @@ def get_seeds(): response.append({ "seed_id": seed.id, "creation_time": seed.creation_time, - "players": seed.multidata["names"] if seed.multidata else [["Singleplayer"]], + "players": [(slot.player_name, slot.game) for slot in seed.slots], }) return jsonify(response) \ No newline at end of file From 44cf8efc061c739c44a453996c4b1b2905690498 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 13 Jan 2022 07:41:31 +0100 Subject: [PATCH 24/32] WebHost: count non-owned Rooms of a given Seed --- WebHostLib/__init__.py | 3 +-- WebHostLib/templates/viewSeed.html | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index 847e99c8..b42ac6a0 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -136,8 +136,7 @@ def view_seed(seed: UUID): seed = Seed.get(id=seed) if not seed: abort(404) - return render_template("viewSeed.html", seed=seed, - rooms=[room for room in seed.rooms if room.owner == session["_id"]]) + return render_template("viewSeed.html", seed=seed) @app.route('/new_room/') diff --git a/WebHostLib/templates/viewSeed.html b/WebHostLib/templates/viewSeed.html index 62763629..deb62617 100644 --- a/WebHostLib/templates/viewSeed.html +++ b/WebHostLib/templates/viewSeed.html @@ -31,12 +31,16 @@ Rooms:  - {% call macros.list_rooms(rooms) %} + {% call macros.list_rooms(seed.rooms | selectattr("owner", "eq", session["_id"])) %}

  • Create New Room
  • {% endcall %} + {% if seed.rooms %} + There are a total of {{ seed.rooms | length }} Rooms, only those created by you are linked above. + {% endif %} + From fba8019f98e42f284208f94feeba62f96283b8b2 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 13 Jan 2022 19:00:29 -0600 Subject: [PATCH 25/32] add requirements mention to plando tutorial --- WebHostLib/static/assets/tutorial/archipelago/plando_en.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/WebHostLib/static/assets/tutorial/archipelago/plando_en.md b/WebHostLib/static/assets/tutorial/archipelago/plando_en.md index 52411c06..25a1b960 100644 --- a/WebHostLib/static/assets/tutorial/archipelago/plando_en.md +++ b/WebHostLib/static/assets/tutorial/archipelago/plando_en.md @@ -13,6 +13,13 @@ On the website plando will already be enabled. If you will be generating the gam * To opt-in go to the archipelago installation (default: `C:\ProgramData\Archipelago`), open the host.yaml with a text editor and find the `plando_options` key. The available plando modules can be enabled by adding them after this such as `plando_options: bosses, items, texts, connections`. +* If you are not the one doing the generation or even if you are you can add to the `requires` section of your yaml so that it will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like: +```yaml + requires: + version: current.version.number + plando: bosses, items, texts, connections +``` + ## Item Plando Item plando allows a player to place an item in a specific location or specific locations, place multiple items into From b8afc27e2f367c4a2f439a2bac8bcd380743c9e2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 14 Jan 2022 19:27:44 +0100 Subject: [PATCH 26/32] Docs: improve "sending_visible" comment --- Main.py | 2 +- worlds/AutoWorld.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Main.py b/Main.py index 5d7ea8e8..cc851ed6 100644 --- a/Main.py +++ b/Main.py @@ -258,7 +258,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No precollected_items = {player: [item.code for item in world_precollected] for player, world_precollected in world.precollected_items.items()} precollected_hints = {player: set() for player in range(1, world.players + 1)} - # for now special case Factorio tech_tree_information + sending_visible_players = set() for slot in world.player_ids: diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index e745937d..abeb9783 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -115,7 +115,8 @@ class World(metaclass=AutoWorldRegister): item_names: Set[str] # set of all potential item names location_names: Set[str] # set of all potential location names - # If there is visibility in what is being sent, this is where it will be known. + # If the game displays all contained items to the user, this flag pre-fills the hint system with this information + # For example the "full" tech tree information option in Factorio sending_visible: bool = False def __init__(self, world: MultiWorld, player: int): From 6641d428a2511fa6ee984e64ae393b41341822f9 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Fri, 14 Jan 2022 09:58:25 -0500 Subject: [PATCH 27/32] oot: check item name for skip child zelda, not the actual item itself --- worlds/oot/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index 7ef75640..c55407b4 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -143,7 +143,7 @@ def set_rules(ootworld): if ootworld.skip_child_zelda: # If skip child zelda is on, the item at Song from Impa must be giveable by the save context. location = world.get_location('Song from Impa', player) - add_item_rule(location, lambda item: item in SaveContext.giveable_items) + add_item_rule(location, lambda item: item.name in SaveContext.giveable_items) for name in ootworld.always_hints: add_rule(world.get_location(name, player), guarantee_hint) From c507efd920853e93e5c5d8fb118e5357a9dc4b16 Mon Sep 17 00:00:00 2001 From: Grrmo <72080072+Grrmo@users.noreply.github.com> Date: Sat, 15 Jan 2022 20:34:49 +0100 Subject: [PATCH 28/32] Corrected mistake in Regions --- worlds/timespinner/Regions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 95cc52b8..04275bfc 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -58,7 +58,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData names: Dict[str, int] = {} - connect(world, player, names, 'Lake desolation', 'Lower lake desolation', lambda state: state._timespinner_has_timestop(world, player or state.has('Talaria Attachment', player))) + connect(world, player, names, 'Lake desolation', 'Lower lake desolation', lambda state: state._timespinner_has_timestop(world, player) or state.has('Talaria Attachment', player)) connect(world, player, names, 'Lake desolation', 'Upper lake desolation', lambda state: state._timespinner_has_fire(world, player) and state.can_reach('Upper Lake Serene', 'Region', player)) connect(world, player, names, 'Lake desolation', 'Skeleton Shaft', lambda state: state._timespinner_has_doublejump(world, player)) connect(world, player, names, 'Lake desolation', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) From 62391d3074839cf7bc1cff2aa5b60f70f9077753 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Fri, 14 Jan 2022 09:40:42 -0500 Subject: [PATCH 29/32] Minecraft tracker: replace bed image url, remove game-complete indicator --- WebHostLib/templates/minecraftTracker.html | 1 - WebHostLib/tracker.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/WebHostLib/templates/minecraftTracker.html b/WebHostLib/templates/minecraftTracker.html index c1d6fa97..2f38822d 100644 --- a/WebHostLib/templates/minecraftTracker.html +++ b/WebHostLib/templates/minecraftTracker.html @@ -43,7 +43,6 @@ - diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index d530f9e0..d28bd671 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -421,7 +421,7 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D "Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png", "Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png", "Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png", - "Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/dc/Red_Bed_JE4_BE3.png", + "Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png", "Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png", "Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png", "Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif", @@ -429,7 +429,6 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D "Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif", "Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png", "Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png", - "Dragon Head": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b6/Dragon_Head.png", } minecraft_location_ids = { From 0dc714f94790baf19f6479515391bd19148ac76b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 15 Jan 2022 21:20:26 +0100 Subject: [PATCH 30/32] Options: fix verify_keys breaking options containing lists of dicts --- Generate.py | 2 -- Options.py | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Generate.py b/Generate.py index 295eb140..45902dc2 100644 --- a/Generate.py +++ b/Generate.py @@ -191,8 +191,6 @@ def main(args=None, callback=ERmain): if len(player_settings.values()) > 1: important[option] = {player: value for player, value in player_settings.items() if player <= args.yaml_output} - elif len(player_settings.values()) > 0: - important[option] = player_settings[1] else: logging.debug(f"No player settings defined for option '{option}'") diff --git a/Options.py b/Options.py index 637f28c6..e8b1b920 100644 --- a/Options.py +++ b/Options.py @@ -251,6 +251,7 @@ class VerifyKeys: @classmethod def verify_keys(cls, data): if cls.valid_keys: + data = set(data) dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data) extra = dataset - cls.valid_keys if extra: @@ -269,7 +270,7 @@ class OptionDict(Option, VerifyKeys): @classmethod def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict: if type(data) == dict: - cls.verify_keys(set(data)) + cls.verify_keys(data) return cls(data) else: raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") @@ -307,7 +308,7 @@ class OptionList(Option, VerifyKeys): @classmethod def from_any(cls, data: typing.Any): if type(data) == list: - cls.verify_keys(set(data)) + cls.verify_keys(data) return cls(data) return cls.from_text(str(data)) From 6a7e1d920aa6b96e1f6601d411dd4ccbb8e6deac Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sat, 15 Jan 2022 19:59:40 -0500 Subject: [PATCH 31/32] User-specified random range (#203) * Add user-specified random range for yaml options --- Options.py | 17 ++++++++++++ .../archipelago/advanced_settings_en.md | 27 ++++++++++++++++--- .../static/assets/tutorial/tutorials.json | 3 ++- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/Options.py b/Options.py index e8b1b920..35c2ed14 100644 --- a/Options.py +++ b/Options.py @@ -210,6 +210,23 @@ class Range(Option, int): return cls(int(round(random.triangular(cls.range_start, cls.range_end, cls.range_end), 0))) elif text == "random-middle": return cls(int(round(random.triangular(cls.range_start, cls.range_end), 0))) + elif text.startswith("random-range-"): + textsplit = text.split("-") + try: + randomrange = [int(textsplit[len(textsplit)-2]), int(textsplit[len(textsplit)-1])] + except ValueError: + raise ValueError(f"Invalid random range {text} for option {cls.__name__}") + randomrange.sort() + if randomrange[0] < cls.range_start or randomrange[1] > cls.range_end: + raise Exception(f"{randomrange[0]}-{randomrange[1]} is outside allowed range {cls.range_start}-{cls.range_end} for option {cls.__name__}") + if text.startswith("random-range-low"): + return cls(int(round(random.triangular(randomrange[0], randomrange[1], randomrange[0])))) + elif text.startswith("random-range-middle"): + return cls(int(round(random.triangular(randomrange[0], randomrange[1])))) + elif text.startswith("random-range-high"): + return cls(int(round(random.triangular(randomrange[0], randomrange[1], randomrange[1])))) + else: + return cls(int(round(random.randint(randomrange[0], randomrange[1])))) else: return cls(random.randint(cls.range_start, cls.range_end)) return cls(int(text)) diff --git a/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md b/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md index e007ce9e..ce533af7 100644 --- a/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md +++ b/WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md @@ -33,8 +33,8 @@ following the colons here are weights. The generator will read the weight of eve times, the next option as many times as its numbered and so forth. For the above example `nested_option_one` will have `option_one_setting_one` 1 time and `option_one_setting_two` 0 times so `option_one_setting_one` is guaranteed to occur. For `nested_option_two`, `option_two_setting_one` will be rolled 14 times and `option_two_setting_two` will be rolled 43 -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. +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 Currently there are only a few options that are root options. Everything else should be nested within one of these root @@ -90,6 +90,19 @@ it to see how important the location is. * `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk" item which isn't necessary for progression to go in these locations. +### Random numbers + +Options taking a choice of a number can also use a variety of `random` options to choose a number randomly. + +* `random` will choose a number allowed for the setting at random +* `random-low` will choose a number allowed for the setting at random, but will be weighted towards lower numbers +* `random-middle` will choose a number allowed for the setting at random, but will be weighted towards the middle of the range +* `random-high` will choose a number allowed for the setting at random, but will be weighted towards higher numbers +* `random-range-#-#` will choose a number at random from between the specified numbers. For example `random-range-40-60` +will choose a number between 40 and 60 +* `random-range-low-#-#`, `random-range-middle-#-#`, and `random-range-high-#-#` will choose a number at random from the +specified numbers, but with the specified weights + ### Example ```yaml @@ -105,6 +118,10 @@ A Link to the Past: smallkey_shuffle: original_dungeon: 1 any_world: 1 + crystals_needed_for_gt: + random-low: 1 + crystals_needed_for_ganon: + random-range-high-1-7: 1 start_inventory: Pegasus Boots: 1 Bombs (3): 2 @@ -144,6 +161,10 @@ things to do. * `smallkey_shuffle` is an option for A Link to the Past which determines how dungeon small keys are shuffled. In this example we have a 1/2 chance for them to either be placed in their original dungeon and a 1/2 chance for them to be placed anywhere amongst the multiworld. +* `crystals_needed_for_gt` determines the number of crystals required to enter the Ganon's Tower entrance. In this example +a random number will be chosen from the allowed range for this setting (0 through 7) but will be weighted towards a lower number. +* `crystals_needed_for_ganon` determines the number of crystals required to beat Ganon. In this example a number +between 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 @@ -157,4 +178,4 @@ that can be used for no cost. * `exclude_locations` forces a not important item to be placed on the `Cave 45` location. * `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. \ No newline at end of file +result. diff --git a/WebHostLib/static/assets/tutorial/tutorials.json b/WebHostLib/static/assets/tutorial/tutorials.json index fa477510..5e764b87 100644 --- a/WebHostLib/static/assets/tutorial/tutorials.json +++ b/WebHostLib/static/assets/tutorial/tutorials.json @@ -25,7 +25,8 @@ "filename": "archipelago/advanced_settings_en.md", "link": "archipelago/advanced_settings/en", "authors": [ - "alwaysintreble" + "alwaysintreble", + "Alchav" ] } ] From e74333cbd391dde70e6f37de4a3eb0e1a959473c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 16 Jan 2022 02:20:37 +0100 Subject: [PATCH 32/32] MultiServer: remove location hinting from !hint and /hint; add /hint_location --- MultiServer.py | 65 +++++++++++++++++++++++++++++++-------------- worlds/AutoWorld.py | 8 +++--- 2 files changed, 49 insertions(+), 24 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 12845121..e431f8b5 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -543,7 +543,10 @@ async def on_client_joined(ctx: Context, client: Client): f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) " f"{verb} {ctx.games[client.slot]} has joined. " f"Client({version_str}), {client.tags}).") - + ctx.notify_client(client, "Now that you are connected, " + "you can use !help to list commands to run via the server." + "If your client supports it, " + "you may have additional local commands you can list with /help.") ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) @@ -1088,7 +1091,7 @@ class ClientMessageProcessor(CommonCommandProcessor): self.output("Cheating is disabled.") return False - def get_hints(self, input_text: str, explicit_location: bool = False) -> bool: + def get_hints(self, input_text: str, for_location: bool = False) -> bool: points_available = get_client_points(self.ctx, self.client) if not input_text: hints = {hint.re_check(self.ctx, self.client.team) for hint in @@ -1100,20 +1103,21 @@ class ClientMessageProcessor(CommonCommandProcessor): return True else: world = proxy_worlds[self.ctx.games[self.client.slot]] - item_name, usable, response = get_intended_text(input_text, - world.all_names if not explicit_location else world.location_names) + names = world.location_names if for_location else world.all_item_and_group_names + hint_name, usable, response = get_intended_text(input_text, + names) if usable: - if item_name in world.hint_blacklist: - self.output(f"Sorry, \"{item_name}\" is marked as non-hintable.") + if hint_name in world.hint_blacklist: + self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") hints = [] - elif item_name in world.item_name_groups and not explicit_location: + elif not for_location and hint_name in world.item_name_groups: # item group name hints = [] - for item in world.item_name_groups[item_name]: + for item in world.item_name_groups[hint_name]: hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item)) - elif item_name in world.item_names and not explicit_location: # item name - hints = collect_hints(self.ctx, self.client.team, self.client.slot, item_name) + elif not for_location and hint_name in world.item_names: # item name + hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name) else: # location name - hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, item_name) + hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, hint_name) cost = self.ctx.get_hint_cost(self.client.slot) if hints: new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot] @@ -1175,8 +1179,8 @@ class ClientMessageProcessor(CommonCommandProcessor): @mark_raw def _cmd_hint(self, item_or_location: str = "") -> bool: - """Use !hint {item_name/location_name}, - for example !hint Lamp or !hint Link's House to get a spoiler peek for that location or item. + """Use !hint {item_name}, + for example !hint Lamp to get a spoiler peek for that item. If hint costs are on, this will only give you one new result, you can rerun the command to get more in that case.""" return self.get_hints(item_or_location) @@ -1511,23 +1515,44 @@ class ServerCommandProcessor(CommonCommandProcessor): self.output(response) return False - def _cmd_hint(self, player_name: str, *item_or_location: str) -> bool: - """Send out a hint for a player's item or location to their team""" + def _cmd_hint(self, player_name: str, *item: str) -> bool: + """Send out a hint for a player's item to their team""" seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) if usable: team, slot = self.ctx.player_name_lookup[seeked_player] - item = " ".join(item_or_location) + item = " ".join(item) world = proxy_worlds[self.ctx.games[slot]] - item, usable, response = get_intended_text(item, world.all_names) + item, usable, response = get_intended_text(item, world.all_item_and_group_names) if usable: if item in world.item_name_groups: hints = [] for item in world.item_name_groups[item]: hints.extend(collect_hints(self.ctx, team, slot, item)) - elif item in world.item_names: # item name + else: # item name hints = collect_hints(self.ctx, team, slot, item) - else: # location name - hints = collect_hints_location(self.ctx, team, slot, item) + if hints: + notify_hints(self.ctx, team, hints) + else: + self.output("No hints found.") + return True + else: + self.output(response) + return False + + else: + self.output(response) + return False + + def _cmd_hint_location(self, player_name: str, *location: str) -> bool: + """Send out a hint for a player's location to their team""" + seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) + if usable: + team, slot = self.ctx.player_name_lookup[seeked_player] + item = " ".join(location) + world = proxy_worlds[self.ctx.games[slot]] + item, usable, response = get_intended_text(item, world.location_names) + if usable: + hints = collect_hints_location(self.ctx, team, slot, item) if hints: notify_hints(self.ctx, team, hints) else: diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index abeb9783..3da65162 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import Dict, Set, Tuple, List, Optional, TextIO +from typing import Dict, Set, Tuple, List, Optional, TextIO, Any from BaseClasses import MultiWorld, Item, CollectionState, Location from Options import Option @@ -8,7 +8,7 @@ from Options import Option class AutoWorldRegister(type): world_types: Dict[str, World] = {} - def __new__(cls, name, bases, dct): + def __new__(cls, name: str, bases, dct: Dict[str, Any]): # filter out any events dct["item_name_to_id"] = {name: id for name, id in dct["item_name_to_id"].items() if id} dct["location_name_to_id"] = {name: id for name, id in dct["location_name_to_id"].items() if id} @@ -19,7 +19,7 @@ class AutoWorldRegister(type): # build rest dct["item_names"] = frozenset(dct["item_name_to_id"]) dct["location_names"] = frozenset(dct["location_name_to_id"]) - dct["all_names"] = dct["item_names"] | dct["location_names"] | set(dct.get("item_name_groups", {})) + dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {}))) # construct class new_class = super().__new__(cls, name, bases, dct) @@ -71,7 +71,7 @@ class World(metaclass=AutoWorldRegister): options: Dict[str, type(Option)] = {} # link your Options mapping game: str # name the game topology_present: bool = False # indicate if world type has any meaningful layout/pathing - all_names: Set[str] = frozenset() # gets automatically populated with all item, item group and location names + all_item_and_group_names: Set[str] = frozenset() # gets automatically populated with all item and item group names # map names to their IDs item_name_to_id: Dict[str, int] = {}