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:
Zach Parks 2024-05-23 02:08:08 -05:00 committed by GitHub
parent 8b6eae0a14
commit e1ff5073b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 83 additions and 124 deletions

View File

@ -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

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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)