Add zone_name to persistence schema
Adds zone_name field to PlayerData and accounts table to track which zone a player is in. Defaults to 'overworld'. Includes migration logic to handle existing databases without the column. Server now resolves zone from zone_name when loading player data.
This commit is contained in:
parent
875ded5762
commit
1349c2f860
2 changed files with 107 additions and 14 deletions
|
|
@ -263,21 +263,35 @@ async def shell(
|
|||
"stamina": 100.0,
|
||||
"max_stamina": 100.0,
|
||||
"flying": False,
|
||||
"zone_name": "overworld",
|
||||
}
|
||||
|
||||
# Resolve zone from zone_name (currently only overworld exists)
|
||||
zone_name = player_data.get("zone_name", "overworld")
|
||||
if zone_name == "overworld":
|
||||
player_zone = _overworld
|
||||
else:
|
||||
# Existing player - verify spawn position is still passable
|
||||
if not _overworld.is_passable(player_data["x"], player_data["y"]):
|
||||
# Saved position is no longer passable, find a new one
|
||||
start_x, start_y = find_passable_start(
|
||||
_overworld, player_data["x"], player_data["y"]
|
||||
)
|
||||
player_data["x"] = start_x
|
||||
player_data["y"] = start_y
|
||||
# Future: lookup zone by name from a zone registry
|
||||
log.warning(
|
||||
"unknown zone '%s' for player '%s', defaulting to overworld",
|
||||
zone_name,
|
||||
player_name,
|
||||
)
|
||||
player_zone = _overworld
|
||||
|
||||
# Verify spawn position is still passable
|
||||
if not player_zone.is_passable(player_data["x"], player_data["y"]):
|
||||
# Saved position is no longer passable, find a new one
|
||||
start_x, start_y = find_passable_start(
|
||||
player_zone, player_data["x"], player_data["y"]
|
||||
)
|
||||
player_data["x"] = start_x
|
||||
player_data["y"] = start_y
|
||||
|
||||
# Create player instance
|
||||
player = Player(
|
||||
name=player_name,
|
||||
location=_overworld,
|
||||
location=player_zone,
|
||||
x=player_data["x"],
|
||||
y=player_data["y"],
|
||||
pl=player_data["pl"],
|
||||
|
|
@ -428,11 +442,6 @@ async def run_server() -> None:
|
|||
)
|
||||
log.info("created overworld zone (%dx%d, toroidal)", _world.width, _world.height)
|
||||
|
||||
# Inject world into command modules
|
||||
mudlib.commands.fly.world = _world
|
||||
mudlib.commands.look.world = _world
|
||||
mudlib.combat.commands.world = _world
|
||||
|
||||
# Load content-defined commands from TOML files
|
||||
content_dir = pathlib.Path(__file__).resolve().parents[2] / "content" / "commands"
|
||||
if content_dir.exists():
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from mudlib.store import (
|
|||
load_player_data,
|
||||
save_player,
|
||||
)
|
||||
from mudlib.zone import Zone
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -177,3 +178,86 @@ def test_password_hashing_different_salts(temp_db):
|
|||
|
||||
# This just verifies the API works correctly - we can't easily check
|
||||
# the hashes are different without exposing internal details
|
||||
|
||||
|
||||
def test_save_and_load_zone_name(temp_db):
|
||||
"""save_player and load_player_data persist zone_name."""
|
||||
create_account("Maria", "password123")
|
||||
|
||||
# Create a zone and player
|
||||
zone = Zone(name="testzone", width=100, height=100)
|
||||
player = Player(
|
||||
name="Maria",
|
||||
location=zone,
|
||||
x=10,
|
||||
y=20,
|
||||
)
|
||||
|
||||
# Save and load
|
||||
save_player(player)
|
||||
data = load_player_data("Maria")
|
||||
|
||||
assert data is not None
|
||||
assert data["zone_name"] == "testzone"
|
||||
|
||||
|
||||
def test_default_zone_name(temp_db):
|
||||
"""New accounts have zone_name default to 'overworld'."""
|
||||
create_account("Noah", "password123")
|
||||
data = load_player_data("Noah")
|
||||
|
||||
assert data is not None
|
||||
assert data["zone_name"] == "overworld"
|
||||
|
||||
|
||||
def test_zone_name_migration(temp_db):
|
||||
"""Existing DB without zone_name column still works."""
|
||||
# Create account, which will create default schema
|
||||
create_account("Olivia", "password123")
|
||||
|
||||
# Simulate old DB by manually removing the zone_name column
|
||||
import sqlite3
|
||||
|
||||
conn = sqlite3.connect(temp_db)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if column exists and remove it
|
||||
cursor.execute("PRAGMA table_info(accounts)")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
if "zone_name" in columns:
|
||||
# SQLite doesn't support DROP COLUMN directly, so recreate table
|
||||
cursor.execute("""
|
||||
CREATE TABLE accounts_backup AS
|
||||
SELECT name, password_hash, salt, x, y, pl, stamina,
|
||||
max_stamina, flying, created_at, last_login
|
||||
FROM accounts
|
||||
""")
|
||||
cursor.execute("DROP TABLE accounts")
|
||||
cursor.execute("""
|
||||
CREATE TABLE accounts (
|
||||
name TEXT PRIMARY KEY COLLATE NOCASE,
|
||||
password_hash TEXT NOT NULL,
|
||||
salt TEXT NOT NULL,
|
||||
x INTEGER NOT NULL DEFAULT 0,
|
||||
y INTEGER NOT NULL DEFAULT 0,
|
||||
pl REAL NOT NULL DEFAULT 100.0,
|
||||
stamina REAL NOT NULL DEFAULT 100.0,
|
||||
max_stamina REAL NOT NULL DEFAULT 100.0,
|
||||
flying INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
last_login TEXT
|
||||
)
|
||||
""")
|
||||
cursor.execute("""
|
||||
INSERT INTO accounts
|
||||
SELECT * FROM accounts_backup
|
||||
""")
|
||||
cursor.execute("DROP TABLE accounts_backup")
|
||||
conn.commit()
|
||||
|
||||
conn.close()
|
||||
|
||||
# Now try to load player data - should handle missing column gracefully
|
||||
data = load_player_data("Olivia")
|
||||
assert data is not None
|
||||
assert data["zone_name"] == "overworld" # Should default
|
||||
|
|
|
|||
Loading…
Reference in a new issue