Add furniture persistence to home zone TOML
This commit is contained in:
parent
acfff671fe
commit
9f760bc3af
2 changed files with 327 additions and 0 deletions
|
|
@ -4,6 +4,10 @@ import logging
|
|||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
from mudlib.entity import Entity
|
||||
from mudlib.portal import Portal
|
||||
from mudlib.thing import Thing
|
||||
from mudlib.things import spawn_thing, thing_templates
|
||||
from mudlib.zone import Zone
|
||||
from mudlib.zones import get_zone, register_zone
|
||||
|
||||
|
|
@ -114,6 +118,22 @@ def save_home_zone(player_name: str, zone: Zone) -> None:
|
|||
lines.append(f"tiles = [{tiles}]")
|
||||
lines.append("")
|
||||
|
||||
# Save furniture (Things in the zone, but not Entities or Portals)
|
||||
furniture = [
|
||||
obj
|
||||
for obj in zone._contents
|
||||
if isinstance(obj, Thing)
|
||||
and not isinstance(obj, Entity)
|
||||
and not isinstance(obj, Portal)
|
||||
]
|
||||
|
||||
for item in furniture:
|
||||
lines.append("[[furniture]]")
|
||||
lines.append(f'template = "{item.name}"')
|
||||
lines.append(f"x = {item.x}")
|
||||
lines.append(f"y = {item.y}")
|
||||
lines.append("")
|
||||
|
||||
path.write_text("\n".join(lines))
|
||||
|
||||
|
||||
|
|
@ -152,6 +172,42 @@ def load_home_zone(player_name: str) -> Zone | None:
|
|||
safe=data.get("safe", True),
|
||||
)
|
||||
|
||||
# Load furniture
|
||||
furniture_list = data.get("furniture", [])
|
||||
for item_data in furniture_list:
|
||||
template_name = item_data.get("template")
|
||||
x = item_data.get("x")
|
||||
y = item_data.get("y")
|
||||
|
||||
if template_name not in thing_templates:
|
||||
log.warning(
|
||||
"Skipping unknown furniture template '%s' in %s",
|
||||
template_name,
|
||||
player_name,
|
||||
)
|
||||
continue
|
||||
|
||||
if not isinstance(x, int) or not isinstance(y, int):
|
||||
log.warning(
|
||||
"Invalid coordinates for furniture '%s' in %s",
|
||||
template_name,
|
||||
player_name,
|
||||
)
|
||||
continue
|
||||
|
||||
if not (0 <= x < zone.width and 0 <= y < zone.height):
|
||||
log.warning(
|
||||
"Out-of-bounds furniture '%s' at (%d,%d) in %s",
|
||||
template_name,
|
||||
x,
|
||||
y,
|
||||
player_name,
|
||||
)
|
||||
continue
|
||||
|
||||
template = thing_templates[template_name]
|
||||
spawn_thing(template, zone, x=x, y=y)
|
||||
|
||||
register_zone(zone.name, zone)
|
||||
return zone
|
||||
|
||||
|
|
|
|||
271
tests/test_furniture.py
Normal file
271
tests/test_furniture.py
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
"""Tests for furniture persistence in home zones."""
|
||||
|
||||
import logging
|
||||
import tomllib
|
||||
|
||||
import pytest
|
||||
|
||||
from mudlib.entity import Entity
|
||||
from mudlib.housing import (
|
||||
create_home_zone,
|
||||
init_housing,
|
||||
load_home_zone,
|
||||
save_home_zone,
|
||||
)
|
||||
from mudlib.portal import Portal
|
||||
from mudlib.thing import Thing
|
||||
from mudlib.things import ThingTemplate, spawn_thing, thing_templates
|
||||
from mudlib.zones import zone_registry
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_registries():
|
||||
"""Clear registries between tests."""
|
||||
saved_zones = dict(zone_registry)
|
||||
saved_templates = dict(thing_templates)
|
||||
zone_registry.clear()
|
||||
thing_templates.clear()
|
||||
yield
|
||||
zone_registry.clear()
|
||||
zone_registry.update(saved_zones)
|
||||
thing_templates.clear()
|
||||
thing_templates.update(saved_templates)
|
||||
|
||||
|
||||
def test_save_furniture_in_zone_toml(tmp_path):
|
||||
"""save_home_zone() writes furniture to TOML."""
|
||||
init_housing(tmp_path)
|
||||
|
||||
# Create zone
|
||||
zone = create_home_zone("Alice")
|
||||
|
||||
# Add a thing template
|
||||
table_template = ThingTemplate(
|
||||
name="table",
|
||||
description="A wooden table",
|
||||
portable=False,
|
||||
)
|
||||
thing_templates["table"] = table_template
|
||||
|
||||
# Spawn furniture in the zone
|
||||
spawn_thing(table_template, zone, x=3, y=4)
|
||||
|
||||
# Save
|
||||
save_home_zone("Alice", zone)
|
||||
|
||||
# Read the TOML file
|
||||
zone_file = tmp_path / "alice.toml"
|
||||
with open(zone_file, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
|
||||
# Check furniture section
|
||||
assert "furniture" in data
|
||||
assert len(data["furniture"]) == 1
|
||||
assert data["furniture"][0]["template"] == "table"
|
||||
assert data["furniture"][0]["x"] == 3
|
||||
assert data["furniture"][0]["y"] == 4
|
||||
|
||||
|
||||
def test_load_furniture_from_toml(tmp_path):
|
||||
"""load_home_zone() spawns furniture from TOML."""
|
||||
init_housing(tmp_path)
|
||||
|
||||
# Create zone to get the file
|
||||
_ = create_home_zone("Bob")
|
||||
zone_file = tmp_path / "bob.toml"
|
||||
|
||||
# Add furniture entries to the TOML
|
||||
with open(zone_file) as f:
|
||||
content = f.read()
|
||||
content += """
|
||||
[[furniture]]
|
||||
template = "chair"
|
||||
x = 5
|
||||
y = 6
|
||||
"""
|
||||
with open(zone_file, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
# Add template
|
||||
chair_template = ThingTemplate(
|
||||
name="chair",
|
||||
description="A wooden chair",
|
||||
portable=True,
|
||||
)
|
||||
thing_templates["chair"] = chair_template
|
||||
|
||||
# Clear registry and load
|
||||
zone_registry.clear()
|
||||
loaded = load_home_zone("Bob")
|
||||
|
||||
assert loaded is not None
|
||||
|
||||
# Check that furniture was spawned
|
||||
chairs = [
|
||||
obj
|
||||
for obj in loaded._contents
|
||||
if isinstance(obj, Thing) and obj.name == "chair"
|
||||
]
|
||||
assert len(chairs) == 1
|
||||
assert chairs[0].x == 5
|
||||
assert chairs[0].y == 6
|
||||
assert chairs[0].description == "A wooden chair"
|
||||
|
||||
|
||||
def test_furniture_round_trip(tmp_path):
|
||||
"""Furniture survives save -> load cycle."""
|
||||
init_housing(tmp_path)
|
||||
|
||||
# Create zone
|
||||
zone = create_home_zone("Charlie")
|
||||
|
||||
# Add templates
|
||||
table_template = ThingTemplate(name="table", description="A table", portable=False)
|
||||
chair_template = ThingTemplate(name="chair", description="A chair", portable=True)
|
||||
thing_templates["table"] = table_template
|
||||
thing_templates["chair"] = chair_template
|
||||
|
||||
# Spawn furniture
|
||||
spawn_thing(table_template, zone, x=3, y=4)
|
||||
spawn_thing(chair_template, zone, x=3, y=5)
|
||||
|
||||
# Save
|
||||
save_home_zone("Charlie", zone)
|
||||
|
||||
# Clear registry and load
|
||||
zone_registry.clear()
|
||||
loaded = load_home_zone("Charlie")
|
||||
|
||||
assert loaded is not None
|
||||
|
||||
# Check furniture
|
||||
tables = [obj for obj in loaded._contents if obj.name == "table"]
|
||||
chairs = [obj for obj in loaded._contents if obj.name == "chair"]
|
||||
|
||||
assert len(tables) == 1
|
||||
assert tables[0].x == 3
|
||||
assert tables[0].y == 4
|
||||
|
||||
assert len(chairs) == 1
|
||||
assert chairs[0].x == 3
|
||||
assert chairs[0].y == 5
|
||||
|
||||
|
||||
def test_multiple_furniture_items(tmp_path):
|
||||
"""Multiple furniture items save and load correctly."""
|
||||
init_housing(tmp_path)
|
||||
|
||||
zone = create_home_zone("Dave")
|
||||
|
||||
# Add templates
|
||||
chair_template = ThingTemplate(name="chair", description="A chair", portable=True)
|
||||
thing_templates["chair"] = chair_template
|
||||
|
||||
# Spawn multiple chairs
|
||||
spawn_thing(chair_template, zone, x=2, y=2)
|
||||
spawn_thing(chair_template, zone, x=3, y=2)
|
||||
spawn_thing(chair_template, zone, x=4, y=2)
|
||||
|
||||
# Save
|
||||
save_home_zone("Dave", zone)
|
||||
|
||||
# Load
|
||||
zone_registry.clear()
|
||||
loaded = load_home_zone("Dave")
|
||||
|
||||
assert loaded is not None
|
||||
|
||||
chairs = [obj for obj in loaded._contents if obj.name == "chair"]
|
||||
assert len(chairs) == 3
|
||||
|
||||
# Check positions
|
||||
positions = {(c.x, c.y) for c in chairs}
|
||||
assert positions == {(2, 2), (3, 2), (4, 2)}
|
||||
|
||||
|
||||
def test_load_unknown_template_skips(tmp_path, caplog):
|
||||
"""Unknown template name in TOML is skipped with warning."""
|
||||
caplog.set_level(logging.WARNING)
|
||||
init_housing(tmp_path)
|
||||
|
||||
# Create zone
|
||||
_ = create_home_zone("Eve")
|
||||
zone_file = tmp_path / "eve.toml"
|
||||
|
||||
# Add furniture with unknown template
|
||||
with open(zone_file) as f:
|
||||
content = f.read()
|
||||
content += """
|
||||
[[furniture]]
|
||||
template = "unknown_thing"
|
||||
x = 1
|
||||
y = 1
|
||||
"""
|
||||
with open(zone_file, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
# Load
|
||||
zone_registry.clear()
|
||||
loaded = load_home_zone("Eve")
|
||||
|
||||
assert loaded is not None
|
||||
|
||||
# Check that no furniture was spawned
|
||||
things = [obj for obj in loaded._contents if isinstance(obj, Thing)]
|
||||
assert len(things) == 0
|
||||
|
||||
# Check that warning was logged
|
||||
assert "unknown_thing" in caplog.text.lower()
|
||||
|
||||
|
||||
def test_save_excludes_entities(tmp_path):
|
||||
"""Entities in zone are NOT saved as furniture."""
|
||||
init_housing(tmp_path)
|
||||
|
||||
zone = create_home_zone("Frank")
|
||||
|
||||
# Add an entity to the zone
|
||||
_ = Entity(name="test_mob", location=zone, x=5, y=5)
|
||||
|
||||
# Save
|
||||
save_home_zone("Frank", zone)
|
||||
|
||||
# Read the TOML
|
||||
zone_file = tmp_path / "frank.toml"
|
||||
with open(zone_file, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
|
||||
# Furniture section should not exist or be empty
|
||||
furniture = data.get("furniture", [])
|
||||
assert len(furniture) == 0
|
||||
|
||||
|
||||
def test_save_excludes_portals(tmp_path):
|
||||
"""Portals are NOT saved as furniture."""
|
||||
init_housing(tmp_path)
|
||||
|
||||
zone = create_home_zone("Grace")
|
||||
|
||||
# Add a portal to the zone
|
||||
_ = Portal(
|
||||
name="exit",
|
||||
description="An exit",
|
||||
location=zone,
|
||||
x=1,
|
||||
y=1,
|
||||
target_zone="overworld",
|
||||
target_x=10,
|
||||
target_y=10,
|
||||
)
|
||||
|
||||
# Save
|
||||
save_home_zone("Grace", zone)
|
||||
|
||||
# Read the TOML
|
||||
zone_file = tmp_path / "grace.toml"
|
||||
with open(zone_file, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
|
||||
# Furniture section should not exist or be empty
|
||||
furniture = data.get("furniture", [])
|
||||
assert len(furniture) == 0
|
||||
Loading…
Reference in a new issue