rom24-quickmud-python 1.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. mud/__init__.py +0 -0
  2. mud/__main__.py +40 -0
  3. mud/account/__init__.py +20 -0
  4. mud/account/account_manager.py +62 -0
  5. mud/account/account_service.py +80 -0
  6. mud/advancement.py +62 -0
  7. mud/affects/saves.py +123 -0
  8. mud/agent/__init__.py +0 -0
  9. mud/agent/agent_protocol.py +19 -0
  10. mud/agent/character_agent.py +61 -0
  11. mud/combat/__init__.py +3 -0
  12. mud/combat/engine.py +189 -0
  13. mud/commands/__init__.py +3 -0
  14. mud/commands/admin_commands.py +77 -0
  15. mud/commands/advancement.py +36 -0
  16. mud/commands/alias_cmds.py +44 -0
  17. mud/commands/build.py +18 -0
  18. mud/commands/combat.py +16 -0
  19. mud/commands/communication.py +55 -0
  20. mud/commands/decorators.py +11 -0
  21. mud/commands/dispatcher.py +206 -0
  22. mud/commands/healer.py +81 -0
  23. mud/commands/help.py +14 -0
  24. mud/commands/imc.py +19 -0
  25. mud/commands/inspection.py +113 -0
  26. mud/commands/inventory.py +42 -0
  27. mud/commands/movement.py +71 -0
  28. mud/commands/notes.py +44 -0
  29. mud/commands/shop.py +138 -0
  30. mud/commands/socials.py +34 -0
  31. mud/config.py +59 -0
  32. mud/db/__init__.py +0 -0
  33. mud/db/init.py +27 -0
  34. mud/db/migrate_from_files.py +87 -0
  35. mud/db/migrations.py +7 -0
  36. mud/db/models.py +98 -0
  37. mud/db/seed.py +28 -0
  38. mud/db/session.py +11 -0
  39. mud/devtools/__init__.py +0 -0
  40. mud/devtools/agent_demo.py +19 -0
  41. mud/entrypoint.py +34 -0
  42. mud/game_loop.py +117 -0
  43. mud/imc/__init__.py +17 -0
  44. mud/imc/protocol.py +32 -0
  45. mud/loaders/__init__.py +27 -0
  46. mud/loaders/area_loader.py +73 -0
  47. mud/loaders/base_loader.py +38 -0
  48. mud/loaders/help_loader.py +17 -0
  49. mud/loaders/json_area_loader.py +203 -0
  50. mud/loaders/json_loader.py +285 -0
  51. mud/loaders/mob_loader.py +104 -0
  52. mud/loaders/obj_loader.py +76 -0
  53. mud/loaders/reset_loader.py +29 -0
  54. mud/loaders/room_loader.py +63 -0
  55. mud/loaders/shop_loader.py +41 -0
  56. mud/loaders/social_loader.py +16 -0
  57. mud/loaders/specials_loader.py +63 -0
  58. mud/logging/__init__.py +0 -0
  59. mud/logging/admin.py +40 -0
  60. mud/logging/agent_trace.py +9 -0
  61. mud/math/c_compat.py +27 -0
  62. mud/mobprog.py +72 -0
  63. mud/models/__init__.py +106 -0
  64. mud/models/area.py +33 -0
  65. mud/models/area_json.py +27 -0
  66. mud/models/board.py +49 -0
  67. mud/models/board_json.py +16 -0
  68. mud/models/character.py +195 -0
  69. mud/models/character_json.py +46 -0
  70. mud/models/constants.py +423 -0
  71. mud/models/conversion.py +45 -0
  72. mud/models/help.py +28 -0
  73. mud/models/help_json.py +14 -0
  74. mud/models/json_io.py +64 -0
  75. mud/models/mob.py +82 -0
  76. mud/models/note.py +29 -0
  77. mud/models/note_json.py +16 -0
  78. mud/models/obj.py +82 -0
  79. mud/models/object.py +28 -0
  80. mud/models/object_json.py +40 -0
  81. mud/models/player_json.py +29 -0
  82. mud/models/room.py +86 -0
  83. mud/models/room_json.py +46 -0
  84. mud/models/shop.py +21 -0
  85. mud/models/shop_json.py +17 -0
  86. mud/models/skill.py +25 -0
  87. mud/models/skill_json.py +20 -0
  88. mud/models/social.py +78 -0
  89. mud/models/social_json.py +20 -0
  90. mud/net/__init__.py +9 -0
  91. mud/net/ansi.py +27 -0
  92. mud/net/connection.py +174 -0
  93. mud/net/protocol.py +57 -0
  94. mud/net/session.py +21 -0
  95. mud/net/telnet_server.py +31 -0
  96. mud/network/__init__.py +0 -0
  97. mud/network/websocket_server.py +83 -0
  98. mud/network/websocket_session.py +21 -0
  99. mud/notes.py +45 -0
  100. mud/persistence.py +185 -0
  101. mud/registry.py +5 -0
  102. mud/scripts/convert_are_to_json.py +162 -0
  103. mud/scripts/convert_help_are_to_json.py +92 -0
  104. mud/scripts/convert_player_to_json.py +112 -0
  105. mud/scripts/convert_shops_to_json.py +64 -0
  106. mud/scripts/convert_skills_to_json.py +166 -0
  107. mud/scripts/convert_social_are_to_json.py +92 -0
  108. mud/scripts/load_test_data.py +17 -0
  109. mud/security/__init__.py +0 -0
  110. mud/security/bans.py +112 -0
  111. mud/security/hash_utils.py +20 -0
  112. mud/server.py +8 -0
  113. mud/skills/__init__.py +3 -0
  114. mud/skills/handlers.py +795 -0
  115. mud/skills/registry.py +97 -0
  116. mud/spawning/__init__.py +1 -0
  117. mud/spawning/mob_spawner.py +13 -0
  118. mud/spawning/obj_spawner.py +18 -0
  119. mud/spawning/reset_handler.py +222 -0
  120. mud/spawning/templates.py +63 -0
  121. mud/spec_funs.py +57 -0
  122. mud/time.py +48 -0
  123. mud/utils/rng_mm.py +123 -0
  124. mud/wiznet.py +74 -0
  125. mud/world/__init__.py +11 -0
  126. mud/world/linking.py +31 -0
  127. mud/world/look.py +29 -0
  128. mud/world/movement.py +135 -0
  129. mud/world/world_state.py +179 -0
  130. rom24_quickmud_python-1.2.2.dist-info/METADATA +236 -0
  131. rom24_quickmud_python-1.2.2.dist-info/RECORD +135 -0
  132. rom24_quickmud_python-1.2.2.dist-info/WHEEL +5 -0
  133. rom24_quickmud_python-1.2.2.dist-info/entry_points.txt +2 -0
  134. rom24_quickmud_python-1.2.2.dist-info/licenses/LICENSE +21 -0
  135. rom24_quickmud_python-1.2.2.dist-info/top_level.txt +1 -0
mud/db/models.py ADDED
@@ -0,0 +1,98 @@
1
+ from sqlalchemy import Column, Integer, String, ForeignKey, Boolean
2
+ from sqlalchemy.orm import relationship, declarative_base
3
+
4
+ Base = declarative_base()
5
+
6
+ class Area(Base):
7
+ __tablename__ = "areas"
8
+ id = Column(Integer, primary_key=True)
9
+ vnum = Column(Integer, unique=True)
10
+ name = Column(String)
11
+ min_vnum = Column(Integer)
12
+ max_vnum = Column(Integer)
13
+ rooms = relationship("Room", back_populates="area")
14
+
15
+ class Room(Base):
16
+ __tablename__ = "rooms"
17
+ id = Column(Integer, primary_key=True)
18
+ vnum = Column(Integer, unique=True)
19
+ name = Column(String)
20
+ description = Column(String)
21
+ sector_type = Column(Integer)
22
+ room_flags = Column(Integer)
23
+ area_id = Column(Integer, ForeignKey("areas.id"))
24
+ area = relationship("Area", back_populates="rooms")
25
+ exits = relationship("Exit", back_populates="room")
26
+
27
+ class Exit(Base):
28
+ __tablename__ = "exits"
29
+ id = Column(Integer, primary_key=True)
30
+ room_id = Column(Integer, ForeignKey("rooms.id"))
31
+ direction = Column(String)
32
+ to_room_vnum = Column(Integer)
33
+ room = relationship("Room", back_populates="exits")
34
+
35
+ class MobPrototype(Base):
36
+ __tablename__ = "mob_prototypes"
37
+ id = Column(Integer, primary_key=True)
38
+ vnum = Column(Integer, unique=True)
39
+ name = Column(String)
40
+ short_desc = Column(String)
41
+ long_desc = Column(String)
42
+ level = Column(Integer)
43
+ alignment = Column(Integer)
44
+
45
+ class ObjPrototype(Base):
46
+ __tablename__ = "obj_prototypes"
47
+ id = Column(Integer, primary_key=True)
48
+ vnum = Column(Integer, unique=True)
49
+ name = Column(String)
50
+ short_desc = Column(String)
51
+ long_desc = Column(String)
52
+ item_type = Column(Integer)
53
+ flags = Column(Integer)
54
+ value0 = Column(Integer)
55
+ value1 = Column(Integer)
56
+ value2 = Column(Integer)
57
+ value3 = Column(Integer)
58
+
59
+
60
+ class ObjectInstance(Base):
61
+ __tablename__ = "object_instances"
62
+ id = Column(Integer, primary_key=True)
63
+ prototype_vnum = Column(Integer, ForeignKey("obj_prototypes.vnum"))
64
+ location = Column(String)
65
+ character_id = Column(Integer, ForeignKey("characters.id"))
66
+
67
+ prototype = relationship("ObjPrototype")
68
+ character = relationship("Character", back_populates="objects")
69
+
70
+
71
+ class PlayerAccount(Base):
72
+ __tablename__ = "player_accounts"
73
+ id = Column(Integer, primary_key=True)
74
+ username = Column(String, unique=True)
75
+ email = Column(String)
76
+ password_hash = Column(String)
77
+ is_admin = Column(Boolean, default=False)
78
+
79
+ characters = relationship("Character", back_populates="player")
80
+
81
+ def set_password(self, password: str):
82
+ """Set the password hash for this account."""
83
+ from mud.security.hash_utils import hash_password
84
+ self.password_hash = hash_password(password)
85
+
86
+
87
+ class Character(Base):
88
+ __tablename__ = "characters"
89
+ id = Column(Integer, primary_key=True)
90
+ name = Column(String, unique=True)
91
+ level = Column(Integer)
92
+ hp = Column(Integer)
93
+ room_vnum = Column(Integer)
94
+
95
+ player_id = Column(Integer, ForeignKey("player_accounts.id"))
96
+ player = relationship("PlayerAccount", back_populates="characters")
97
+ objects = relationship("ObjectInstance", back_populates="character")
98
+
mud/db/seed.py ADDED
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from mud.db.session import SessionLocal
4
+ from mud.db.models import PlayerAccount, Character
5
+ from mud.security.hash_utils import hash_password
6
+
7
+
8
+ def create_test_account():
9
+ session = SessionLocal()
10
+ if session.query(PlayerAccount).filter_by(username="admin").first():
11
+ session.close()
12
+ return
13
+ account = PlayerAccount(
14
+ username="admin",
15
+ password_hash=hash_password("admin"),
16
+ is_admin=True,
17
+ )
18
+ char = Character(
19
+ name="Testman",
20
+ level=1,
21
+ hp=100,
22
+ room_vnum=3001,
23
+ player=account,
24
+ )
25
+ session.add(account)
26
+ session.add(char)
27
+ session.commit()
28
+ session.close()
mud/db/session.py ADDED
@@ -0,0 +1,11 @@
1
+ import os
2
+ from sqlalchemy import create_engine
3
+ from sqlalchemy.orm import sessionmaker
4
+
5
+ DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///mud.db")
6
+
7
+ engine = create_engine(
8
+ DATABASE_URL,
9
+ connect_args={"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {},
10
+ )
11
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
File without changes
@@ -0,0 +1,19 @@
1
+ from mud.registry import room_registry, mob_registry
2
+ from mud.spawning.mob_spawner import spawn_mob
3
+ from mud.agent.character_agent import CharacterAgentAdapter
4
+
5
+
6
+ def run_agent_demo() -> None:
7
+ room = room_registry.get(3001)
8
+ proto_vnum = next(iter(mob_registry)) if mob_registry else None
9
+ if proto_vnum is None or room is None:
10
+ print("World not initialized or no mobs available")
11
+ return
12
+ mob = spawn_mob(proto_vnum)
13
+ if not mob:
14
+ print("Failed to spawn mob")
15
+ return
16
+ adapter = CharacterAgentAdapter(mob)
17
+ room.add_mob(mob)
18
+ print(adapter.get_observation())
19
+ print(adapter.perform_action("say", ["I", "am", "alive!"]))
mud/entrypoint.py ADDED
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from mud.account.account_service import create_account, login
4
+
5
+
6
+ def prompt_login():
7
+ print("Welcome to the Realm.")
8
+ username = input("Username: ")
9
+ password = input("Password: ")
10
+
11
+ account = login(username, password)
12
+ if not account:
13
+ print("❌ Invalid login.")
14
+ return None
15
+
16
+ print(f"✅ Logged in as {username}")
17
+ return account
18
+
19
+
20
+ def prompt_account_creation():
21
+ print("Create your account:")
22
+ username = input("Username: ")
23
+ password = input("Password: ")
24
+ confirm = input("Confirm Password: ")
25
+ if password != confirm:
26
+ print("❌ Passwords do not match.")
27
+ return None
28
+
29
+ success = create_account(username, password)
30
+ if success:
31
+ print("✅ Account created.")
32
+ else:
33
+ print("❌ Username already taken.")
34
+ return success
mud/game_loop.py ADDED
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Callable, List
5
+
6
+ from mud.models.character import Character, character_registry
7
+ from mud.skills.registry import skill_registry
8
+ from mud.spawning.reset_handler import reset_tick
9
+ from mud.time import time_info
10
+ from mud.config import get_pulse_tick, get_pulse_violence, GAME_LOOP_STRICT_POINT
11
+ from mud.net.protocol import broadcast_global
12
+ from mud.logging.admin import rotate_admin_log
13
+ from mud.spec_funs import run_npc_specs
14
+
15
+
16
+ @dataclass
17
+ class WeatherState:
18
+ """Very small placeholder for global weather."""
19
+ sky: str = "sunny"
20
+
21
+
22
+ weather = WeatherState()
23
+
24
+
25
+ @dataclass
26
+ class TimedEvent:
27
+ ticks: int
28
+ callback: Callable[[], None]
29
+
30
+
31
+ events: List[TimedEvent] = []
32
+
33
+
34
+ def schedule_event(ticks: int, callback: Callable[[], None]) -> None:
35
+ """Schedule a callback to run after a number of ticks."""
36
+ events.append(TimedEvent(ticks, callback))
37
+
38
+
39
+ def event_tick() -> None:
40
+ """Advance timers and fire ready callbacks."""
41
+ for ev in events[:]:
42
+ ev.ticks -= 1
43
+ if ev.ticks <= 0:
44
+ ev.callback()
45
+ events.remove(ev)
46
+
47
+
48
+ def regen_character(ch: Character) -> None:
49
+ """Apply a single tick of regeneration to a character."""
50
+ ch.hit = min(ch.max_hit, ch.hit + 1)
51
+ ch.mana = min(ch.max_mana, ch.mana + 1)
52
+ ch.move = min(ch.max_move, ch.move + 1)
53
+ skill_registry.tick(ch)
54
+
55
+
56
+ def regen_tick() -> None:
57
+ for ch in list(character_registry):
58
+ regen_character(ch)
59
+
60
+
61
+ _WEATHER_STATES = ["sunny", "cloudy", "rainy"]
62
+
63
+
64
+ def weather_tick() -> None:
65
+ """Cycle through simple weather states."""
66
+ index = _WEATHER_STATES.index(weather.sky)
67
+ weather.sky = _WEATHER_STATES[(index + 1) % len(_WEATHER_STATES)]
68
+
69
+
70
+ def time_tick() -> None:
71
+ """Advance world time and broadcast day/night transitions."""
72
+ messages = time_info.advance_hour()
73
+ if time_info.hour == 0:
74
+ try:
75
+ rotate_admin_log()
76
+ except Exception:
77
+ pass
78
+ for message in messages:
79
+ broadcast_global(message, channel="info")
80
+
81
+
82
+ _pulse_counter = 0
83
+ _violence_counter = 0
84
+
85
+
86
+ def violence_tick() -> None:
87
+ """Violence cadence updates: decrement wait/daze counters."""
88
+ for ch in list(character_registry):
89
+ ch.wait = max(0, int(getattr(ch, "wait", 0)) - 1)
90
+ # daze is optional in some tests; default to attr when present
91
+ if hasattr(ch, "daze"):
92
+ ch.daze = max(0, int(getattr(ch, "daze", 0)) - 1)
93
+
94
+
95
+ def game_tick() -> None:
96
+ """Run a full game tick: time, regen, weather, timed events, and resets."""
97
+ global _pulse_counter, _violence_counter
98
+ _pulse_counter += 1
99
+ # Violence cadence: decrement wait/daze on PULSE_VIOLENCE boundaries
100
+ # This mirrors ROM's update_handler calling violence_update.
101
+ _violence_counter += 1
102
+ if _violence_counter % get_pulse_violence() == 0:
103
+ violence_tick()
104
+ # Advance time/weather/resets on point pulses, preserving legacy behavior when not strict
105
+ point_pulse = (_pulse_counter % get_pulse_tick() == 0)
106
+ if point_pulse:
107
+ time_tick()
108
+ weather_tick()
109
+ reset_tick()
110
+ else:
111
+ if not GAME_LOOP_STRICT_POINT:
112
+ weather_tick()
113
+ reset_tick()
114
+ regen_tick()
115
+ event_tick()
116
+ # Invoke NPC special functions after resets to mirror ROM's update cadence
117
+ run_npc_specs()
mud/imc/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+
6
+ def imc_enabled() -> bool:
7
+ """Feature flag for IMC. Disabled by default."""
8
+ return os.getenv("IMC_ENABLED", "false").lower() in {"1", "true", "yes"}
9
+
10
+
11
+ def maybe_open_socket() -> None:
12
+ """No-op when IMC is disabled. Never opens sockets in disabled mode."""
13
+ if not imc_enabled():
14
+ return None
15
+ # Intentionally unimplemented: networking is out of scope for P0 stub.
16
+ raise NotImplementedError("IMC networking not implemented in stub")
17
+
mud/imc/protocol.py ADDED
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class Frame:
8
+ """Minimal IMC-like frame for testing parser/serializer.
9
+
10
+ Format (text line):
11
+ "<type> <from> <to> :<message>"
12
+ Example:
13
+ "chat alice@quickmud * :Hello world"
14
+ """
15
+
16
+ type: str
17
+ source: str
18
+ target: str
19
+ message: str
20
+
21
+
22
+ def parse_frame(line: str) -> Frame:
23
+ parts = line.strip().split(" ", 3)
24
+ if len(parts) < 4 or not parts[3].startswith(":"):
25
+ raise ValueError("invalid IMC frame: expected '<type> <from> <to> :<message>'")
26
+ ftype, source, target, msg = parts
27
+ return Frame(type=ftype, source=source, target=target, message=msg[1:])
28
+
29
+
30
+ def serialize_frame(frame: Frame) -> str:
31
+ return f"{frame.type} {frame.source} {frame.target} :{frame.message}"
32
+
@@ -0,0 +1,27 @@
1
+ from .area_loader import load_area_file
2
+ from .json_area_loader import load_all_areas_from_json
3
+ from pathlib import Path
4
+
5
+ from mud.registry import area_registry, room_registry, mob_registry, obj_registry, shop_registry
6
+
7
+
8
+ def load_all_areas(list_path: str = "area/area.lst"):
9
+ # Clear all registries before loading to prevent conflicts
10
+ area_registry.clear()
11
+ room_registry.clear()
12
+ mob_registry.clear()
13
+ obj_registry.clear()
14
+ shop_registry.clear()
15
+ sentinel_found = False
16
+ with open(list_path, 'r', encoding='latin-1') as f:
17
+ for line in f:
18
+ line = line.strip()
19
+ if not line or line.startswith('#'):
20
+ continue
21
+ if line == '$':
22
+ sentinel_found = True
23
+ break
24
+ path = Path('area') / line
25
+ load_area_file(str(path))
26
+ if not sentinel_found:
27
+ raise ValueError("area.lst missing '$' sentinel")
@@ -0,0 +1,73 @@
1
+ from .base_loader import BaseTokenizer
2
+ from .room_loader import load_rooms
3
+ from .mob_loader import load_mobiles
4
+ from .obj_loader import load_objects
5
+ from .reset_loader import load_resets
6
+ from .shop_loader import load_shops
7
+ from .specials_loader import load_specials
8
+ from mud.models.area import Area
9
+ from mud.registry import area_registry
10
+
11
+ SECTION_HANDLERS = {
12
+ "#ROOMS": load_rooms,
13
+ "#MOBILES": load_mobiles,
14
+ "#OBJECTS": load_objects,
15
+ "#RESETS": load_resets,
16
+ "#SHOPS": load_shops,
17
+ "#SPECIALS": load_specials,
18
+ }
19
+
20
+
21
+ def load_area_file(filepath: str) -> Area:
22
+ with open(filepath, 'r', encoding='latin-1') as f:
23
+ lines = f.readlines()
24
+ tokenizer = BaseTokenizer(lines)
25
+ area = Area()
26
+ while True:
27
+ line = tokenizer.next_line()
28
+ if line is None:
29
+ break
30
+ if line == '#AREA':
31
+ area.file_name = tokenizer.next_line().rstrip('~')
32
+ area.name = tokenizer.next_line().rstrip('~')
33
+ area.credits = tokenizer.next_line().rstrip('~')
34
+ vnums = tokenizer.next_line()
35
+ if vnums:
36
+ parts = vnums.split()
37
+ if len(parts) >= 2:
38
+ area.min_vnum = int(parts[0])
39
+ area.max_vnum = int(parts[1])
40
+ elif line in SECTION_HANDLERS:
41
+ handler = SECTION_HANDLERS[line]
42
+ handler(tokenizer, area)
43
+ elif line == "#AREADATA":
44
+ while True:
45
+ peek = tokenizer.peek_line()
46
+ if peek is None or peek.startswith('#'):
47
+ break
48
+ data_line = tokenizer.next_line()
49
+ if data_line.startswith('Builders'):
50
+ area.builders = data_line.split(None, 1)[1].rstrip('~')
51
+ elif data_line.startswith('Security'):
52
+ parts = data_line.split()
53
+ if len(parts) > 1:
54
+ area.security = int(parts[1])
55
+ elif data_line.startswith('Flags'):
56
+ parts = data_line.split()
57
+ if len(parts) > 1:
58
+ area.area_flags = int(parts[1])
59
+ elif line.startswith('#$') or line == '$':
60
+ break
61
+ key = area.min_vnum
62
+ area.vnum = area.min_vnum
63
+ # START enforce unique area vnum
64
+ if (
65
+ key != 0
66
+ and key in area_registry
67
+ and area_registry[key].file_name != area.file_name
68
+ ):
69
+ raise ValueError(f"duplicate area vnum {key}")
70
+ # END enforce unique area vnum
71
+ area_registry[key] = area
72
+
73
+ return area
@@ -0,0 +1,38 @@
1
+ class BaseTokenizer:
2
+ """Simple tokenizer for area files."""
3
+ def __init__(self, lines):
4
+ self.lines = [line.rstrip('\n') for line in lines]
5
+ self.index = 0
6
+
7
+ def next_line(self):
8
+ while self.index < len(self.lines):
9
+ line = self.lines[self.index].strip()
10
+ self.index += 1
11
+ if line.startswith('*') or line == '':
12
+ continue
13
+ return line
14
+ return None
15
+
16
+ def peek_line(self):
17
+ pos = self.index
18
+ line = self.next_line()
19
+ self.index = pos
20
+ return line
21
+
22
+ def read_string_tilde(self):
23
+ parts = []
24
+ while True:
25
+ line = self.next_line()
26
+ if line is None:
27
+ break
28
+ if line.endswith('~'):
29
+ parts.append(line[:-1])
30
+ break
31
+ parts.append(line)
32
+ return '\n'.join(parts)
33
+
34
+ def read_number(self):
35
+ line = self.next_line()
36
+ if line is None:
37
+ raise ValueError('Unexpected EOF while reading number')
38
+ return int(line.split()[0])
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from mud.models.help import HelpEntry, register_help, help_registry
7
+ from mud.models.help_json import HelpJson
8
+
9
+
10
+ def load_help_file(path: str | Path) -> None:
11
+ """Load help entries from ``path`` into ``help_registry``."""
12
+ with open(path, "r", encoding="utf-8") as fp:
13
+ data = json.load(fp)
14
+ help_registry.clear()
15
+ for raw in data:
16
+ entry = HelpEntry.from_json(HelpJson.from_dict(raw))
17
+ register_help(entry)