Fix IF bugs: case-insensitive story lookup, double prompt, phantom restore command

- _find_story() now compares path.stem.lower() so "lostpig" matches "LostPig.z8"
- Server no longer writes its own prompt in IF mode (game handles prompting)
- Suppress phantom game output on restore (saved PC past sread causes garbage)
- Route .z5/.z8 files to EmbeddedIFSession now that V5+ is supported
This commit is contained in:
Jared Miller 2026-02-10 14:16:19 -05:00
parent 14816478aa
commit 602da45ac2
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
4 changed files with 19 additions and 19 deletions

View file

@ -24,7 +24,7 @@ def _find_story(name: str) -> pathlib.Path | None:
# prefix match (e.g. "zork" matches "zork1.z3") # prefix match (e.g. "zork" matches "zork1.z3")
if _stories_dir.exists(): if _stories_dir.exists():
for path in sorted(_stories_dir.iterdir()): for path in sorted(_stories_dir.iterdir()):
if path.stem.startswith(name) and path.suffix in _STORY_EXTENSIONS: if path.stem.lower().startswith(name) and path.suffix in _STORY_EXTENSIONS:
return path return path
return None return None
@ -65,8 +65,8 @@ async def cmd_play(player: Player, args: str) -> None:
if not isinstance(story_path, pathlib.Path): if not isinstance(story_path, pathlib.Path):
story_path = pathlib.Path(story_path) story_path = pathlib.Path(story_path)
# Use embedded interpreter for z3 files, dfrotz for others # Use embedded interpreter for z-machine files, dfrotz for others
if story_path.suffix == ".z3": if story_path.suffix in (".z3", ".z5", ".z8"):
try: try:
session = EmbeddedIFSession(player, str(story_path), game_name) session = EmbeddedIFSession(player, str(story_path), game_name)
except (FileNotFoundError, OSError) as e: except (FileNotFoundError, OSError) as e:

View file

@ -81,8 +81,10 @@ class EmbeddedIFSession:
output = self._screen.flush() output = self._screen.flush()
if restored: if restored:
prefix = "restoring saved game...\r\nrestored." # After restore, the game processes phantom input (garbage in text
return f"{prefix}\r\n\r\n{output}" if output else prefix # buffer), producing unwanted output. Suppress it and only show the
# restore confirmation.
return "restoring saved game...\r\nrestored."
return output return output
async def handle_input(self, text: str) -> IFResponse: async def handle_input(self, text: str) -> IFResponse:

View file

@ -315,7 +315,8 @@ async def shell(
if player.mode == "editor" and player.editor: if player.mode == "editor" and player.editor:
_writer.write(f" {player.editor.cursor + 1}> ") _writer.write(f" {player.editor.cursor + 1}> ")
elif player.mode == "if" and player.if_session: elif player.mode == "if" and player.if_session:
_writer.write("\r\n> ") # IF mode: game writes its own prompt, don't add another
pass
else: else:
_writer.write("mud> ") _writer.write("mud> ")
await _writer.drain() await _writer.drain()

View file

@ -67,10 +67,9 @@ async def test_play_enters_if_mode(player):
mock_session.save_path = Mock(spec=Path) mock_session.save_path = Mock(spec=Path)
mock_session.save_path.exists = Mock(return_value=False) mock_session.save_path.exists = Mock(return_value=False)
with patch("mudlib.commands.play.IFSession") as MockIFSession: with patch("mudlib.commands.play.EmbeddedIFSession") as MockSession:
MockIFSession.return_value = mock_session MockSession.return_value = mock_session
# Use .z5 to test dfrotz path
with patch("mudlib.commands.play._find_story") as mock_find: with patch("mudlib.commands.play._find_story") as mock_find:
mock_find.return_value = "/fake/path/zork1.z5" mock_find.return_value = "/fake/path/zork1.z5"
@ -108,11 +107,11 @@ async def test_play_handles_dfrotz_missing(player):
with patch("mudlib.commands.play.IFSession") as MockIFSession: with patch("mudlib.commands.play.IFSession") as MockIFSession:
MockIFSession.return_value = mock_session MockIFSession.return_value = mock_session
# Use .z5 to test dfrotz path # Use .zblorb to test dfrotz path (z3/z5/z8 go to embedded)
with patch("mudlib.commands.play._find_story") as mock_find: with patch("mudlib.commands.play._find_story") as mock_find:
mock_find.return_value = "/fake/path/zork1.z5" mock_find.return_value = "/fake/path/game.zblorb"
await cmd_play(player, "zork1") await cmd_play(player, "game")
# Verify error message was sent # Verify error message was sent
player.writer.write.assert_called() player.writer.write.assert_called()
@ -142,10 +141,9 @@ async def test_play_restores_save_if_exists(player):
) )
mock_session.start = AsyncMock(return_value=restored_output) mock_session.start = AsyncMock(return_value=restored_output)
with patch("mudlib.commands.play.IFSession") as MockIFSession: with patch("mudlib.commands.play.EmbeddedIFSession") as MockSession:
MockIFSession.return_value = mock_session MockSession.return_value = mock_session
# Use .z5 to test dfrotz path
with patch("mudlib.commands.play._find_story") as mock_find: with patch("mudlib.commands.play._find_story") as mock_find:
mock_find.return_value = "/fake/path/zork1.z5" mock_find.return_value = "/fake/path/zork1.z5"
@ -170,14 +168,13 @@ async def test_play_no_restore_if_no_save(player):
from mudlib.commands.play import cmd_play from mudlib.commands.play import cmd_play
# Mock IFSession # Mock EmbeddedIFSession
mock_session = Mock() mock_session = Mock()
mock_session.start = AsyncMock(return_value="Welcome to Zork!") mock_session.start = AsyncMock(return_value="Welcome to Zork!")
with patch("mudlib.commands.play.IFSession") as MockIFSession: with patch("mudlib.commands.play.EmbeddedIFSession") as MockSession:
MockIFSession.return_value = mock_session MockSession.return_value = mock_session
# Use .z5 to test dfrotz path
with patch("mudlib.commands.play._find_story") as mock_find: with patch("mudlib.commands.play._find_story") as mock_find:
mock_find.return_value = "/fake/path/zork1.z5" mock_find.return_value = "/fake/path/zork1.z5"