Add furniture persistence to home zone TOML

This commit is contained in:
Jared Miller 2026-02-14 17:36:49 -05:00
parent acfff671fe
commit 9f760bc3af
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 327 additions and 0 deletions

View file

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