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
|
import tomllib
|
||||||
from pathlib import Path
|
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.zone import Zone
|
||||||
from mudlib.zones import get_zone, register_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(f"tiles = [{tiles}]")
|
||||||
lines.append("")
|
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))
|
path.write_text("\n".join(lines))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -152,6 +172,42 @@ def load_home_zone(player_name: str) -> Zone | None:
|
||||||
safe=data.get("safe", True),
|
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)
|
register_zone(zone.name, zone)
|
||||||
return 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