WebHost, Core: Move item and location descriptions to `WebWorld` responsibilities. (#2508)
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									8b6eae0a14
								
							
						
					
					
						commit
						e1ff5073b5
					
				|  | @ -121,6 +121,53 @@ class RLWeb(WebWorld): | ||||||
|     # ... |     # ... | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | * `location_descriptions` (optional) WebWorlds can provide a map that contains human-friendly descriptions of locations  | ||||||
|  | or location groups. | ||||||
|  | 
 | ||||||
|  |   ```python | ||||||
|  |   # locations.py | ||||||
|  |   location_descriptions = { | ||||||
|  |       "Red Potion #6": "In a secret destructible block under the second stairway", | ||||||
|  |       "L2 Spaceship": """ | ||||||
|  |         The group of all items in the spaceship in Level 2. | ||||||
|  |    | ||||||
|  |         This doesn't include the item on the spaceship door, since it can be | ||||||
|  |         accessed without the Spaceship Key. | ||||||
|  |       """ | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   # __init__.py | ||||||
|  |   from worlds.AutoWorld import WebWorld | ||||||
|  |   from .locations import location_descriptions | ||||||
|  |    | ||||||
|  |    | ||||||
|  |   class MyGameWeb(WebWorld): | ||||||
|  |       location_descriptions = location_descriptions | ||||||
|  |   ``` | ||||||
|  | 
 | ||||||
|  | * `item_descriptions` (optional) WebWorlds can provide a map that contains human-friendly descriptions of items or item  | ||||||
|  | groups. | ||||||
|  | 
 | ||||||
|  |   ```python | ||||||
|  |   # items.py | ||||||
|  |   item_descriptions = { | ||||||
|  |       "Red Potion": "A standard health potion", | ||||||
|  |       "Spaceship Key": """ | ||||||
|  |         The key to the spaceship in Level 2. | ||||||
|  |    | ||||||
|  |         This is necessary to get to the Star Realm. | ||||||
|  |       """, | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   # __init__.py | ||||||
|  |   from worlds.AutoWorld import WebWorld | ||||||
|  |   from .items import item_descriptions | ||||||
|  |    | ||||||
|  |    | ||||||
|  |   class MyGameWeb(WebWorld): | ||||||
|  |       item_descriptions = item_descriptions | ||||||
|  |   ``` | ||||||
|  | 
 | ||||||
| ### MultiWorld Object | ### MultiWorld Object | ||||||
| 
 | 
 | ||||||
| The `MultiWorld` object references the whole multiworld (all items and locations for all players) and is accessible | The `MultiWorld` object references the whole multiworld (all items and locations for all players) and is accessible | ||||||
|  | @ -178,36 +225,6 @@ Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED | ||||||
| The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being | The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being | ||||||
| required, and will prevent progression and useful items from being placed at excluded locations. | required, and will prevent progression and useful items from being placed at excluded locations. | ||||||
| 
 | 
 | ||||||
| #### Documenting Locations |  | ||||||
| 
 |  | ||||||
| Worlds can optionally provide a `location_descriptions` map which contains human-friendly descriptions of locations and |  | ||||||
| location groups. These descriptions will show up in location-selection options on the options pages. |  | ||||||
| 
 |  | ||||||
| ```python |  | ||||||
| # locations.py |  | ||||||
| 
 |  | ||||||
| location_descriptions = { |  | ||||||
|     "Red Potion #6": "In a secret destructible block under the second stairway", |  | ||||||
|     "L2 Spaceship": |  | ||||||
|         """ |  | ||||||
|         The group of all items in the spaceship in Level 2. |  | ||||||
| 
 |  | ||||||
|         This doesn't include the item on the spaceship door, since it can be accessed without the Spaceship Key. |  | ||||||
|         """ |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ```python |  | ||||||
| # __init__.py |  | ||||||
| 
 |  | ||||||
| from worlds.AutoWorld import World |  | ||||||
| from .locations import location_descriptions |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class MyGameWorld(World): |  | ||||||
|     location_descriptions = location_descriptions |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### Items | ### Items | ||||||
| 
 | 
 | ||||||
| Items are all things that can "drop" for your game. This may be RPG items like weapons, or technologies you normally | Items are all things that can "drop" for your game. This may be RPG items like weapons, or technologies you normally | ||||||
|  | @ -232,36 +249,6 @@ Other classifications include: | ||||||
| * `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that | * `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that | ||||||
|   will not be moved around by progression balancing; used, e.g., for currency or tokens, to not flood early spheres |   will not be moved around by progression balancing; used, e.g., for currency or tokens, to not flood early spheres | ||||||
| 
 | 
 | ||||||
| #### Documenting Items |  | ||||||
| 
 |  | ||||||
| Worlds can optionally provide an `item_descriptions` map which contains human-friendly descriptions of items and item |  | ||||||
| groups. These descriptions will show up in item-selection options on the options pages. |  | ||||||
| 
 |  | ||||||
| ```python |  | ||||||
| # items.py |  | ||||||
| 
 |  | ||||||
| item_descriptions = { |  | ||||||
|     "Red Potion": "A standard health potion", |  | ||||||
|     "Spaceship Key": |  | ||||||
|         """ |  | ||||||
|         The key to the spaceship in Level 2. |  | ||||||
| 
 |  | ||||||
|         This is necessary to get to the Star Realm. |  | ||||||
|         """ |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ```python |  | ||||||
| # __init__.py |  | ||||||
| 
 |  | ||||||
| from worlds.AutoWorld import World |  | ||||||
| from .items import item_descriptions |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class MyGameWorld(World): |  | ||||||
|     item_descriptions = item_descriptions |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### Events | ### Events | ||||||
| 
 | 
 | ||||||
| An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to | An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to | ||||||
|  |  | ||||||
|  | @ -64,15 +64,6 @@ class TestBase(unittest.TestCase): | ||||||
|                 for item in multiworld.itempool: |                 for item in multiworld.itempool: | ||||||
|                     self.assertIn(item.name, world_type.item_name_to_id) |                     self.assertIn(item.name, world_type.item_name_to_id) | ||||||
| 
 | 
 | ||||||
|     def test_item_descriptions_have_valid_names(self): |  | ||||||
|         """Ensure all item descriptions match an item name or item group name""" |  | ||||||
|         for game_name, world_type in AutoWorldRegister.world_types.items(): |  | ||||||
|             valid_names = world_type.item_names.union(world_type.item_name_groups) |  | ||||||
|             for name in world_type.item_descriptions: |  | ||||||
|                 with self.subTest("Name should be valid", game=game_name, item=name): |  | ||||||
|                     self.assertIn(name, valid_names, |  | ||||||
|                                   "All item descriptions must match defined item names") |  | ||||||
| 
 |  | ||||||
|     def test_itempool_not_modified(self): |     def test_itempool_not_modified(self): | ||||||
|         """Test that worlds don't modify the itempool after `create_items`""" |         """Test that worlds don't modify the itempool after `create_items`""" | ||||||
|         gen_steps = ("generate_early", "create_regions", "create_items") |         gen_steps = ("generate_early", "create_regions", "create_items") | ||||||
|  |  | ||||||
|  | @ -66,12 +66,3 @@ class TestBase(unittest.TestCase): | ||||||
|                         for location in locations: |                         for location in locations: | ||||||
|                             self.assertIn(location, world_type.location_name_to_id) |                             self.assertIn(location, world_type.location_name_to_id) | ||||||
|                         self.assertNotIn(group_name, world_type.location_name_to_id) |                         self.assertNotIn(group_name, world_type.location_name_to_id) | ||||||
| 
 |  | ||||||
|     def test_location_descriptions_have_valid_names(self): |  | ||||||
|         """Ensure all location descriptions match a location name or location group name""" |  | ||||||
|         for game_name, world_type in AutoWorldRegister.world_types.items(): |  | ||||||
|             valid_names = world_type.location_names.union(world_type.location_name_groups) |  | ||||||
|             for name in world_type.location_descriptions: |  | ||||||
|                 with self.subTest("Name should be valid", game=game_name, location=name): |  | ||||||
|                     self.assertIn(name, valid_names, |  | ||||||
|                                   "All location descriptions must match defined location names") |  | ||||||
|  |  | ||||||
|  | @ -0,0 +1,23 @@ | ||||||
|  | import unittest | ||||||
|  | 
 | ||||||
|  | from worlds.AutoWorld import AutoWorldRegister | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TestWebDescriptions(unittest.TestCase): | ||||||
|  |     def test_item_descriptions_have_valid_names(self) -> None: | ||||||
|  |         """Ensure all item descriptions match an item name or item group name""" | ||||||
|  |         for game_name, world_type in AutoWorldRegister.world_types.items(): | ||||||
|  |             valid_names = world_type.item_names.union(world_type.item_name_groups) | ||||||
|  |             for name in world_type.web.item_descriptions: | ||||||
|  |                 with self.subTest("Name should be valid", game=game_name, item=name): | ||||||
|  |                     self.assertIn(name, valid_names, | ||||||
|  |                                   "All item descriptions must match defined item names") | ||||||
|  | 
 | ||||||
|  |     def test_location_descriptions_have_valid_names(self) -> None: | ||||||
|  |         """Ensure all location descriptions match a location name or location group name""" | ||||||
|  |         for game_name, world_type in AutoWorldRegister.world_types.items(): | ||||||
|  |             valid_names = world_type.location_names.union(world_type.location_name_groups) | ||||||
|  |             for name in world_type.web.location_descriptions: | ||||||
|  |                 with self.subTest("Name should be valid", game=game_name, location=name): | ||||||
|  |                     self.assertIn(name, valid_names, | ||||||
|  |                                   "All location descriptions must match defined location names") | ||||||
|  | @ -3,13 +3,12 @@ from __future__ import annotations | ||||||
| import hashlib | import hashlib | ||||||
| import logging | import logging | ||||||
| import pathlib | import pathlib | ||||||
| from random import Random |  | ||||||
| import re |  | ||||||
| import sys | import sys | ||||||
| import time | import time | ||||||
|  | from random import Random | ||||||
| from dataclasses import make_dataclass | from dataclasses import make_dataclass | ||||||
| from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, | from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple, | ||||||
|                     Optional, Set, TextIO, Tuple, TYPE_CHECKING, Type, Union) |                     TYPE_CHECKING, Type, Union) | ||||||
| 
 | 
 | ||||||
| from Options import ( | from Options import ( | ||||||
|     ExcludeLocations, ItemLinks, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions, |     ExcludeLocations, ItemLinks, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions, | ||||||
|  | @ -55,17 +54,12 @@ class AutoWorldRegister(type): | ||||||
|         dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set |         dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set | ||||||
|                                    in dct.get("item_name_groups", {}).items()} |                                    in dct.get("item_name_groups", {}).items()} | ||||||
|         dct["item_name_groups"]["Everything"] = dct["item_names"] |         dct["item_name_groups"]["Everything"] = dct["item_names"] | ||||||
|         dct["item_descriptions"] = {name: _normalize_description(description) for name, description | 
 | ||||||
|                                     in dct.get("item_descriptions", {}).items()} |  | ||||||
|         dct["item_descriptions"]["Everything"] = "All items in the entire game." |  | ||||||
|         dct["location_names"] = frozenset(dct["location_name_to_id"]) |         dct["location_names"] = frozenset(dct["location_name_to_id"]) | ||||||
|         dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set |         dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set | ||||||
|                                        in dct.get("location_name_groups", {}).items()} |                                        in dct.get("location_name_groups", {}).items()} | ||||||
|         dct["location_name_groups"]["Everywhere"] = dct["location_names"] |         dct["location_name_groups"]["Everywhere"] = dct["location_names"] | ||||||
|         dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {}))) |         dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {}))) | ||||||
|         dct["location_descriptions"] = {name: _normalize_description(description) for name, description |  | ||||||
|                                     in dct.get("location_descriptions", {}).items()} |  | ||||||
|         dct["location_descriptions"]["Everywhere"] = "All locations in the entire game." |  | ||||||
| 
 | 
 | ||||||
|         # move away from get_required_client_version function |         # move away from get_required_client_version function | ||||||
|         if "game" in dct: |         if "game" in dct: | ||||||
|  | @ -226,6 +220,12 @@ class WebWorld(metaclass=WebWorldRegister): | ||||||
|     option_groups: ClassVar[List[OptionGroup]] = [] |     option_groups: ClassVar[List[OptionGroup]] = [] | ||||||
|     """Ordered list of option groupings. Any options not set in a group will be placed in a pre-built "Game Options".""" |     """Ordered list of option groupings. Any options not set in a group will be placed in a pre-built "Game Options".""" | ||||||
| 
 | 
 | ||||||
|  |     location_descriptions: Dict[str, str] = {} | ||||||
|  |     """An optional map from location names (or location group names) to brief descriptions for users.""" | ||||||
|  | 
 | ||||||
|  |     item_descriptions: Dict[str, str] = {} | ||||||
|  |     """An optional map from item names (or item group names) to brief descriptions for users.""" | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class World(metaclass=AutoWorldRegister): | class World(metaclass=AutoWorldRegister): | ||||||
|     """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. |     """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. | ||||||
|  | @ -252,23 +252,9 @@ class World(metaclass=AutoWorldRegister): | ||||||
|     item_name_groups: ClassVar[Dict[str, Set[str]]] = {} |     item_name_groups: ClassVar[Dict[str, Set[str]]] = {} | ||||||
|     """maps item group names to sets of items. Example: {"Weapons": {"Sword", "Bow"}}""" |     """maps item group names to sets of items. Example: {"Weapons": {"Sword", "Bow"}}""" | ||||||
| 
 | 
 | ||||||
|     item_descriptions: ClassVar[Dict[str, str]] = {} |  | ||||||
|     """An optional map from item names (or item group names) to brief descriptions for users. |  | ||||||
| 
 |  | ||||||
|     Individual newlines and indentation will be collapsed into spaces before these descriptions are |  | ||||||
|     displayed. This may cover only a subset of items. |  | ||||||
|     """ |  | ||||||
| 
 |  | ||||||
|     location_name_groups: ClassVar[Dict[str, Set[str]]] = {} |     location_name_groups: ClassVar[Dict[str, Set[str]]] = {} | ||||||
|     """maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}""" |     """maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}""" | ||||||
| 
 | 
 | ||||||
|     location_descriptions: ClassVar[Dict[str, str]] = {} |  | ||||||
|     """An optional map from location names (or location group names) to brief descriptions for users. |  | ||||||
| 
 |  | ||||||
|     Individual newlines and indentation will be collapsed into spaces before these descriptions are |  | ||||||
|     displayed. This may cover only a subset of locations. |  | ||||||
|     """ |  | ||||||
| 
 |  | ||||||
|     data_version: ClassVar[int] = 0 |     data_version: ClassVar[int] = 0 | ||||||
|     """ |     """ | ||||||
|     Increment this every time something in your world's names/id mappings changes. |     Increment this every time something in your world's names/id mappings changes. | ||||||
|  | @ -572,18 +558,3 @@ def data_package_checksum(data: "GamesPackage") -> str: | ||||||
|     assert sorted(data) == list(data), "Data not ordered" |     assert sorted(data) == list(data), "Data not ordered" | ||||||
|     from NetUtils import encode |     from NetUtils import encode | ||||||
|     return hashlib.sha1(encode(data).encode()).hexdigest() |     return hashlib.sha1(encode(data).encode()).hexdigest() | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def _normalize_description(description): |  | ||||||
|     """ |  | ||||||
|     Normalizes a description in item_descriptions or location_descriptions. |  | ||||||
| 
 |  | ||||||
|     This allows authors to write descritions with nice indentation and line lengths in their world |  | ||||||
|     definitions without having it affect the rendered format. |  | ||||||
|     """ |  | ||||||
|     # First, collapse the whitespace around newlines and the ends of the description. |  | ||||||
|     description = re.sub(r' *\n *', '\n', description.strip()) |  | ||||||
|     # Next, condense individual newlines into spaces. |  | ||||||
|     description = re.sub(r'(?<!\n)\n(?!\n)', ' ', description) |  | ||||||
|     return description |  | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -1272,11 +1272,7 @@ _cut_content_items = [DS3ItemData(row[0], row[1], False, row[2]) for row in [ | ||||||
| ]] | ]] | ||||||
| 
 | 
 | ||||||
| item_descriptions = { | item_descriptions = { | ||||||
|     "Cinders": """ |     "Cinders": "All four Cinders of a Lord.\n\nOnce you have these four, you can fight Soul of Cinder and win the game.", | ||||||
|       All four Cinders of a Lord. |  | ||||||
| 
 |  | ||||||
|       Once you have these four, you can fight Soul of Cinder and win the game. |  | ||||||
|     """, |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| _all_items = _vanilla_items + _dlc_items | _all_items = _vanilla_items + _dlc_items | ||||||
|  |  | ||||||
|  | @ -35,6 +35,8 @@ class DarkSouls3Web(WebWorld): | ||||||
| 
 | 
 | ||||||
|     tutorials = [setup_en, setup_fr] |     tutorials = [setup_en, setup_fr] | ||||||
| 
 | 
 | ||||||
|  |     item_descriptions = item_descriptions | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class DarkSouls3World(World): | class DarkSouls3World(World): | ||||||
|     """ |     """ | ||||||
|  | @ -61,8 +63,6 @@ class DarkSouls3World(World): | ||||||
|             "Cinders of a Lord - Lothric Prince" |             "Cinders of a Lord - Lothric Prince" | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     item_descriptions = item_descriptions |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|     def __init__(self, multiworld: MultiWorld, player: int): |     def __init__(self, multiworld: MultiWorld, player: int): | ||||||
|         super().__init__(multiworld, player) |         super().__init__(multiworld, player) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue