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/persistence.py ADDED
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional
6
+ import os
7
+
8
+ from mud.models.character import Character, character_registry
9
+ from mud.models.json_io import dump_dataclass, load_dataclass
10
+ from mud.spawning.obj_spawner import spawn_object
11
+ from mud.registry import room_registry
12
+ from mud.time import time_info, Sunlight
13
+
14
+
15
+ @dataclass
16
+ class PlayerSave:
17
+ """Serializable snapshot of a player's state."""
18
+
19
+ name: str
20
+ level: int
21
+ hit: int
22
+ max_hit: int
23
+ mana: int
24
+ max_mana: int
25
+ move: int
26
+ max_move: int
27
+ gold: int
28
+ silver: int
29
+ exp: int
30
+ position: int
31
+ # ROM bitfields to preserve flags parity
32
+ affected_by: int = 0
33
+ wiznet: int = 0
34
+ room_vnum: Optional[int] = None
35
+ inventory: List[int] = field(default_factory=list)
36
+ equipment: Dict[str, int] = field(default_factory=dict)
37
+ aliases: Dict[str, str] = field(default_factory=dict)
38
+
39
+
40
+ PLAYERS_DIR = Path("data/players")
41
+ TIME_FILE = Path("data/time.json")
42
+
43
+
44
+ def save_character(char: Character) -> None:
45
+ """Persist ``char`` to ``PLAYERS_DIR`` as JSON."""
46
+ PLAYERS_DIR.mkdir(parents=True, exist_ok=True)
47
+ data = PlayerSave(
48
+ name=char.name or "",
49
+ level=char.level,
50
+ hit=char.hit,
51
+ max_hit=char.max_hit,
52
+ mana=char.mana,
53
+ max_mana=char.max_mana,
54
+ move=char.move,
55
+ max_move=char.max_move,
56
+ gold=char.gold,
57
+ silver=char.silver,
58
+ exp=char.exp,
59
+ position=char.position,
60
+ affected_by=getattr(char, "affected_by", 0),
61
+ wiznet=getattr(char, "wiznet", 0),
62
+ room_vnum=char.room.vnum if getattr(char, "room", None) else None,
63
+ inventory=[obj.prototype.vnum for obj in char.inventory],
64
+ equipment={slot: obj.prototype.vnum for slot, obj in char.equipment.items()},
65
+ aliases=dict(getattr(char, "aliases", {})),
66
+ )
67
+ path = PLAYERS_DIR / f"{char.name.lower()}.json"
68
+ tmp_path = path.with_suffix(".tmp")
69
+ with tmp_path.open("w") as f:
70
+ dump_dataclass(data, f, indent=2)
71
+ f.flush()
72
+ os.fsync(f.fileno())
73
+ os.replace(tmp_path, path)
74
+
75
+
76
+ def load_character(name: str) -> Optional[Character]:
77
+ """Load a character by ``name`` from ``PLAYERS_DIR``."""
78
+ path = PLAYERS_DIR / f"{name.lower()}.json"
79
+ if not path.exists():
80
+ return None
81
+ with path.open() as f:
82
+ data = load_dataclass(PlayerSave, f)
83
+ char = Character(
84
+ name=data.name,
85
+ level=data.level,
86
+ hit=data.hit,
87
+ max_hit=data.max_hit,
88
+ mana=data.mana,
89
+ max_mana=data.max_mana,
90
+ move=data.move,
91
+ max_move=data.max_move,
92
+ gold=data.gold,
93
+ silver=data.silver,
94
+ exp=data.exp,
95
+ position=data.position,
96
+ )
97
+ # restore bitfields
98
+ char.affected_by = getattr(data, "affected_by", 0)
99
+ char.wiznet = getattr(data, "wiznet", 0)
100
+ if data.room_vnum is not None:
101
+ room = room_registry.get(data.room_vnum)
102
+ if room:
103
+ room.add_character(char)
104
+ for vnum in data.inventory:
105
+ obj = spawn_object(vnum)
106
+ if obj:
107
+ char.add_object(obj)
108
+ for slot, vnum in data.equipment.items():
109
+ obj = spawn_object(vnum)
110
+ if obj:
111
+ char.equip_object(obj, slot)
112
+ # restore aliases
113
+ try:
114
+ char.aliases.update(getattr(data, "aliases", {}) or {})
115
+ except Exception:
116
+ pass
117
+ character_registry.append(char)
118
+ return char
119
+
120
+
121
+ def save_world() -> None:
122
+ """Write all registered characters to disk."""
123
+ save_time_info()
124
+ for char in list(character_registry):
125
+ if char.name:
126
+ save_character(char)
127
+
128
+
129
+ def load_world() -> List[Character]:
130
+ """Load all character files from ``PLAYERS_DIR``."""
131
+ chars: List[Character] = []
132
+ load_time_info()
133
+ if not PLAYERS_DIR.exists():
134
+ return chars
135
+ for path in PLAYERS_DIR.glob("*.json"):
136
+ char = load_character(path.stem)
137
+ if char:
138
+ chars.append(char)
139
+ return chars
140
+
141
+
142
+ # --- Time persistence ---
143
+
144
+ @dataclass
145
+ class TimeSave:
146
+ hour: int
147
+ day: int
148
+ month: int
149
+ year: int
150
+ sunlight: int
151
+
152
+
153
+ def save_time_info() -> None:
154
+ """Persist global time_info to TIME_FILE (atomic write)."""
155
+ TIME_FILE.parent.mkdir(parents=True, exist_ok=True)
156
+ data = TimeSave(
157
+ hour=time_info.hour,
158
+ day=time_info.day,
159
+ month=time_info.month,
160
+ year=time_info.year,
161
+ sunlight=int(time_info.sunlight),
162
+ )
163
+ tmp_path = TIME_FILE.with_suffix(".tmp")
164
+ with tmp_path.open("w") as f:
165
+ dump_dataclass(data, f, indent=2)
166
+ f.flush()
167
+ os.fsync(f.fileno())
168
+ os.replace(tmp_path, TIME_FILE)
169
+
170
+
171
+ def load_time_info() -> None:
172
+ """Load global time_info from TIME_FILE if present."""
173
+ if not TIME_FILE.exists():
174
+ return
175
+ with TIME_FILE.open() as f:
176
+ data = load_dataclass(TimeSave, f)
177
+ time_info.hour = data.hour
178
+ time_info.day = data.day
179
+ time_info.month = data.month
180
+ time_info.year = data.year
181
+ try:
182
+ time_info.sunlight = Sunlight(data.sunlight)
183
+ except Exception:
184
+ # Fallback if invalid value
185
+ time_info.sunlight = Sunlight.DARK
mud/registry.py ADDED
@@ -0,0 +1,5 @@
1
+ room_registry = {}
2
+ mob_registry = {}
3
+ obj_registry = {}
4
+ area_registry = {}
5
+ shop_registry = {}
@@ -0,0 +1,162 @@
1
+ import argparse
2
+ import json
3
+ from pathlib import Path
4
+
5
+ from mud.loaders.area_loader import load_area_file
6
+ from mud.models.constants import Direction, Sector
7
+ from mud.registry import area_registry, room_registry, mob_registry, obj_registry
8
+
9
+
10
+ def clear_registries() -> None:
11
+ """Reset global registries to avoid cross-contamination."""
12
+ area_registry.clear()
13
+ room_registry.clear()
14
+ mob_registry.clear()
15
+ obj_registry.clear()
16
+
17
+
18
+ def room_to_dict(room) -> dict:
19
+ exits = {}
20
+ for idx, exit_obj in enumerate(room.exits):
21
+ if not exit_obj:
22
+ continue
23
+ direction = Direction(idx).name.lower()
24
+ exits[direction] = {
25
+ "to_room": exit_obj.vnum or 0,
26
+ }
27
+ if exit_obj.description:
28
+ exits[direction]["description"] = exit_obj.description
29
+ if exit_obj.keyword:
30
+ exits[direction]["keyword"] = exit_obj.keyword
31
+ exits[direction]["flags"] = getattr(exit_obj, 'flags', '0')
32
+ if exit_obj.key:
33
+ exits[direction]["key"] = exit_obj.key
34
+ extra = []
35
+ for ed in room.extra_descr:
36
+ if ed.keyword and ed.description:
37
+ extra.append({"keyword": ed.keyword, "description": ed.description})
38
+ # Note: Resets are stored at area level, not per-room in ROM format
39
+ try:
40
+ sector = Sector(room.sector_type).name.lower()
41
+ except ValueError:
42
+ sector = str(room.sector_type)
43
+ return {
44
+ "id": room.vnum,
45
+ "name": room.name or "",
46
+ "description": room.description or "",
47
+ "sector_type": sector,
48
+ "flags": room.room_flags or 0,
49
+ "exits": exits,
50
+ "extra_descriptions": extra,
51
+ "area": room.area.vnum if room.area else 0,
52
+ }
53
+
54
+
55
+ def mob_to_dict(mob) -> dict:
56
+ return {
57
+ "id": mob.vnum,
58
+ "name": mob.short_descr or "",
59
+ "player_name": mob.player_name or "",
60
+ "long_description": mob.long_descr or "",
61
+ "description": mob.description or "",
62
+ "race": getattr(mob, 'race', ''),
63
+ "act_flags": getattr(mob, 'act_flags', ''),
64
+ "affected_by": getattr(mob, 'affected_by', ''),
65
+ "alignment": getattr(mob, 'alignment', 0),
66
+ "group": getattr(mob, 'group', 0),
67
+ "level": getattr(mob, 'level', 1),
68
+ "thac0": getattr(mob, 'thac0', 20),
69
+ "ac": getattr(mob, 'ac', '1d1+0'),
70
+ "hit_dice": getattr(mob, 'hit_dice', '1d1+0'),
71
+ "mana_dice": getattr(mob, 'mana_dice', '1d1+0'),
72
+ "damage_dice": getattr(mob, 'damage_dice', '1d4+0'),
73
+ "damage_type": getattr(mob, 'damage_type', 'beating'),
74
+ "ac_pierce": getattr(mob, 'ac_pierce', 0),
75
+ "ac_bash": getattr(mob, 'ac_bash', 0),
76
+ "ac_slash": getattr(mob, 'ac_slash', 0),
77
+ "ac_exotic": getattr(mob, 'ac_exotic', 0),
78
+ "offensive": getattr(mob, 'offensive', ''),
79
+ "immune": getattr(mob, 'immune', ''),
80
+ "resist": getattr(mob, 'resist', ''),
81
+ "vuln": getattr(mob, 'vuln', ''),
82
+ "start_pos": getattr(mob, 'start_pos', 'standing'),
83
+ "default_pos": getattr(mob, 'default_pos', 'standing'),
84
+ "sex": getattr(mob, 'sex', 'neutral'),
85
+ "wealth": getattr(mob, 'wealth', 0),
86
+ "form": getattr(mob, 'form', '0'),
87
+ "parts": getattr(mob, 'parts', '0'),
88
+ "size": getattr(mob, 'size', 'medium'),
89
+ "material": getattr(mob, 'material', '0'),
90
+ }
91
+
92
+ def object_to_dict(obj) -> dict:
93
+ return {
94
+ "id": obj.vnum,
95
+ "name": obj.short_descr or "",
96
+ "description": obj.description or "",
97
+ "material": obj.material or "",
98
+ "item_type": getattr(obj, 'item_type', 'trash'),
99
+ "extra_flags": getattr(obj, 'extra_flags', ''),
100
+ "wear_flags": getattr(obj, 'wear_flags', ''),
101
+ "weight": getattr(obj, 'weight', 0),
102
+ "cost": getattr(obj, 'cost', 0),
103
+ "condition": getattr(obj, 'condition', 'P'),
104
+ "values": getattr(obj, 'value', [0, 0, 0, 0, 0]),
105
+ "affects": getattr(obj, 'affects', []),
106
+ "extra_descriptions": getattr(obj, 'extra_descr', []),
107
+ }
108
+
109
+ def convert_area(path: str) -> dict:
110
+ clear_registries()
111
+ area = load_area_file(path)
112
+ rooms = [room_to_dict(r) for r in room_registry.values() if r.area is area]
113
+ mobiles = [mob_to_dict(m) for m in mob_registry.values() if m.area is area]
114
+ objects = [object_to_dict(o) for o in obj_registry.values() if o.area is area]
115
+ # Extract #SPECIALS mapping from prototypes for persistence
116
+ specials: list[dict] = []
117
+ for m in mob_registry.values():
118
+ if m.area is area and getattr(m, "spec_fun", None):
119
+ specials.append({"mob_vnum": m.vnum, "spec": str(m.spec_fun)})
120
+
121
+ # Convert area-level resets
122
+ resets = []
123
+ for r in area.resets:
124
+ resets.append({
125
+ "command": r.command,
126
+ "arg1": r.arg1,
127
+ "arg2": r.arg2,
128
+ "arg3": r.arg3,
129
+ "arg4": r.arg4,
130
+ })
131
+
132
+ data = {
133
+ "name": area.name or "",
134
+ "vnum_range": {"min": area.min_vnum, "max": area.max_vnum},
135
+ "builders": [b.strip() for b in (area.builders or "").split(",") if b.strip()],
136
+ "rooms": rooms,
137
+ "mobiles": mobiles,
138
+ "objects": objects,
139
+ "resets": resets,
140
+ "specials": specials,
141
+ }
142
+ return data
143
+
144
+
145
+ def main():
146
+ parser = argparse.ArgumentParser(description="Convert ROM .are file to JSON")
147
+ parser.add_argument("input", help="Path to .are file")
148
+ parser.add_argument(
149
+ "--out-dir",
150
+ default=Path("data/areas"),
151
+ type=Path,
152
+ help="Directory to write JSON files (default: data/areas)",
153
+ )
154
+ args = parser.parse_args()
155
+ data = convert_area(args.input)
156
+ out_dir = args.out_dir
157
+ out_dir.mkdir(parents=True, exist_ok=True)
158
+ out_file = out_dir / f"{Path(args.input).stem}.json"
159
+ out_file.write_text(json.dumps(data, indent=2) + "\n")
160
+
161
+ if __name__ == "__main__":
162
+ main()
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+
8
+ def parse_help_are(path: Path) -> list[dict]:
9
+ """Parse a ROM #HELPS section from an .are file into JSON entries.
10
+
11
+ Preserves help text exactly (including spacing and newlines).
12
+ Each entry is a dict with: level:int, keywords:list[str], text:str.
13
+ """
14
+ entries: list[dict] = []
15
+ in_helps = False
16
+ level: int | None = None
17
+ keywords: list[str] | None = None
18
+ buf: list[str] = []
19
+
20
+ def flush_current() -> None:
21
+ nonlocal level, keywords, buf
22
+ if level is None or keywords is None:
23
+ return
24
+ text = "\n".join(buf)
25
+ entries.append({
26
+ "level": level,
27
+ "keywords": keywords,
28
+ "text": text,
29
+ })
30
+ level, keywords, buf = None, None, []
31
+
32
+ with path.open("r", encoding="utf-8", errors="ignore") as fp:
33
+ for raw in fp:
34
+ line = raw.rstrip("\n")
35
+ if not in_helps:
36
+ if line.strip() == "#HELPS":
37
+ in_helps = True
38
+ continue
39
+
40
+ # End of helps section sentinel: `0 $~`
41
+ if line.strip() == "0 $~":
42
+ flush_current()
43
+ break
44
+
45
+ # New entry header: "<level> <keywords>~"
46
+ if level is None and line.strip():
47
+ # Some areas may have spacing/tabs before values; split once.
48
+ try:
49
+ lvl_part, rest = line.strip().split(" ", 1)
50
+ except ValueError:
51
+ # Malformed header; skip gracefully
52
+ continue
53
+ # Header may end with trailing '~'
54
+ rest = rest.rstrip()
55
+ if rest.endswith("~"):
56
+ rest = rest[:-1]
57
+ try:
58
+ level = int(lvl_part)
59
+ except ValueError:
60
+ # Not a numeric level; skip
61
+ level = None
62
+ continue
63
+ # Keywords are space-separated tokens
64
+ keywords = [tok for tok in rest.split() if tok]
65
+ buf = []
66
+ continue
67
+
68
+ # Inside help text body; a line with only '~' terminates the entry
69
+ if level is not None:
70
+ if line.strip() == "~":
71
+ flush_current()
72
+ else:
73
+ buf.append(line)
74
+
75
+ return entries
76
+
77
+
78
+ def main() -> None:
79
+ ap = argparse.ArgumentParser(description="Convert ROM help.are #HELPS to JSON")
80
+ ap.add_argument("infile", type=Path, help="Path to area/help.are")
81
+ ap.add_argument("outfile", type=Path, help="Output JSON path")
82
+ args = ap.parse_args()
83
+
84
+ entries = parse_help_are(args.infile)
85
+ args.outfile.parent.mkdir(parents=True, exist_ok=True)
86
+ with args.outfile.open("w", encoding="utf-8") as fp:
87
+ json.dump(entries, fp, ensure_ascii=False, indent=2)
88
+
89
+
90
+ if __name__ == "__main__": # pragma: no cover
91
+ main()
92
+
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict
4
+ from pathlib import Path
5
+ from typing import Tuple
6
+
7
+ from mud.models.player_json import PlayerJson
8
+
9
+
10
+ def _letters_to_bits(spec: str) -> int:
11
+ """Map a compact ROM letter-spec (e.g., "QT" or "NOP") to a bitmask.
12
+
13
+ Supports 'A'..'Z' → bits 0..25. Extended pairs like 'aa' not required for
14
+ this conversion but can be added later.
15
+ """
16
+ bits = 0
17
+ for ch in spec.strip():
18
+ if 'A' <= ch <= 'Z':
19
+ bits |= 1 << (ord(ch) - ord('A'))
20
+ return bits
21
+
22
+
23
+ def _parse_hmv(tokens: list[str]) -> Tuple[int, int, int, int, int, int]:
24
+ # HMV <hit> <max_hit> <mana> <max_mana> <move> <max_move>
25
+ vals = [int(t) for t in tokens]
26
+ while len(vals) < 6:
27
+ vals.append(0)
28
+ return vals[0], vals[1], vals[2], vals[3], vals[4], vals[5]
29
+
30
+
31
+ def convert_player(path: str | Path) -> PlayerJson:
32
+ name = ""
33
+ level = 0
34
+ room_vnum = None
35
+ hit = max_hit = mana = max_mana = move = max_move = 0
36
+ plr_flags = 0
37
+ comm_flags = 0
38
+
39
+ lines = Path(path).read_text(encoding="latin-1").splitlines()
40
+ # Validate header/footer sentinels
41
+ nonempty = [ln.strip() for ln in lines if ln.strip()]
42
+ if not nonempty or nonempty[0] != "#PLAYER":
43
+ raise ValueError("invalid player file: missing #PLAYER header")
44
+ if "#END" not in nonempty:
45
+ raise ValueError("invalid player file: missing #END footer")
46
+
47
+ for raw in lines:
48
+ line = raw.strip()
49
+ if not line:
50
+ continue
51
+ if line.startswith("Name "):
52
+ name = line.split(" ", 1)[1].rstrip("~")
53
+ elif line.startswith("Levl "):
54
+ try:
55
+ level = int(line.split()[1])
56
+ except Exception as e:
57
+ raise ValueError("invalid Levl field") from e
58
+ elif line.startswith("Room "):
59
+ try:
60
+ room_vnum = int(line.split()[1])
61
+ except Exception as e:
62
+ raise ValueError("invalid Room field") from e
63
+ elif line.startswith("HMV "):
64
+ vals = line.split()[1:]
65
+ if len(vals) != 6:
66
+ raise ValueError("invalid HMV field: expected 6 integers")
67
+ hit, max_hit, mana, max_mana, move, max_move = _parse_hmv(vals)
68
+ elif line.startswith("Act "):
69
+ spec = line.split()[1]
70
+ if not spec.isalpha() or not spec.isupper():
71
+ raise ValueError("invalid Act flags: expected A..Z letters")
72
+ plr_flags = _letters_to_bits(spec)
73
+ elif line.startswith("Comm "):
74
+ spec = line.split()[1]
75
+ if not spec.isalpha() or not spec.isupper():
76
+ raise ValueError("invalid Comm flags: expected A..Z letters")
77
+ comm_flags = _letters_to_bits(spec)
78
+
79
+ return PlayerJson(
80
+ name=name,
81
+ level=level,
82
+ hit=hit,
83
+ max_hit=max_hit,
84
+ mana=mana,
85
+ max_mana=max_mana,
86
+ move=move,
87
+ max_move=max_move,
88
+ gold=0,
89
+ silver=0,
90
+ exp=0,
91
+ position=0,
92
+ room_vnum=room_vnum,
93
+ inventory=[],
94
+ equipment={},
95
+ plr_flags=plr_flags,
96
+ comm_flags=comm_flags,
97
+ )
98
+
99
+
100
+ def main() -> None:
101
+ import argparse
102
+ import json
103
+
104
+ parser = argparse.ArgumentParser(description="Convert ROM player file to JSON")
105
+ parser.add_argument("input", help="Path to legacy player file")
106
+ args = parser.parse_args()
107
+ pj = convert_player(args.input)
108
+ print(json.dumps(asdict(pj), indent=2))
109
+
110
+
111
+ if __name__ == "__main__":
112
+ main()
@@ -0,0 +1,64 @@
1
+ import argparse
2
+ import json
3
+ from pathlib import Path
4
+
5
+ from mud.loaders.area_loader import load_area_file
6
+ from mud.models.constants import ItemType
7
+ from mud.registry import shop_registry, area_registry, room_registry, mob_registry, obj_registry
8
+
9
+
10
+ def clear_registries() -> None:
11
+ """Reset all global registries."""
12
+ shop_registry.clear()
13
+ area_registry.clear()
14
+ room_registry.clear()
15
+ mob_registry.clear()
16
+ obj_registry.clear()
17
+
18
+
19
+ def shop_to_dict(shop) -> dict:
20
+ buy_types = []
21
+ for t in shop.buy_types:
22
+ if t == 0:
23
+ continue
24
+ try:
25
+ buy_types.append(ItemType(t).name.lower())
26
+ except ValueError:
27
+ buy_types.append(str(t))
28
+ return {
29
+ "keeper": shop.keeper,
30
+ "buy_types": buy_types,
31
+ "profit_buy": shop.profit_buy,
32
+ "profit_sell": shop.profit_sell,
33
+ "open_hour": shop.open_hour,
34
+ "close_hour": shop.close_hour,
35
+ }
36
+
37
+
38
+ def convert_shops(area_list: str) -> list[dict]:
39
+ """Load areas listed in ``area_list`` and return shop dicts."""
40
+ clear_registries()
41
+ area_dir = Path(area_list).parent
42
+ with open(area_list, "r", encoding="latin-1") as f:
43
+ for line in f:
44
+ line = line.strip()
45
+ if not line or line.startswith("$"):
46
+ continue
47
+ load_area_file(str(area_dir / line))
48
+ return [shop_to_dict(s) for s in shop_registry.values()]
49
+
50
+
51
+ def main() -> None:
52
+ parser = argparse.ArgumentParser(description="Convert #SHOPS to JSON")
53
+ parser.add_argument("area_list", help="Path to area.lst")
54
+ parser.add_argument(
55
+ "--out", default=Path("data/shops.json"), type=Path, help="Output JSON file"
56
+ )
57
+ args = parser.parse_args()
58
+ data = convert_shops(args.area_list)
59
+ args.out.parent.mkdir(parents=True, exist_ok=True)
60
+ args.out.write_text(json.dumps(data, indent=2) + "\n")
61
+
62
+
63
+ if __name__ == "__main__":
64
+ main()