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
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Convert ROM 2.4 skill_table from const.c to JSON format.
4
+ """
5
+
6
+ import json
7
+ import re
8
+ from pathlib import Path
9
+ from typing import List, Dict, Any
10
+
11
+
12
+ def parse_skill_table(const_c_path: Path) -> List[Dict[str, Any]]:
13
+ """Parse the skill_table from src/const.c and convert to JSON format."""
14
+
15
+ content = const_c_path.read_text(encoding='latin-1')
16
+
17
+ # Find the skill_table definition
18
+ start_match = re.search(r'const struct skill_type skill_table\[MAX_SKILL\] = \{', content)
19
+ if not start_match:
20
+ raise ValueError("Could not find skill_table definition")
21
+
22
+ # Extract the skill table content
23
+ start_pos = start_match.end()
24
+ brace_count = 1
25
+ pos = start_pos
26
+
27
+ while pos < len(content) and brace_count > 0:
28
+ if content[pos] == '{':
29
+ brace_count += 1
30
+ elif content[pos] == '}':
31
+ brace_count -= 1
32
+ pos += 1
33
+
34
+ if brace_count != 0:
35
+ raise ValueError("Could not find end of skill_table")
36
+
37
+ table_content = content[start_pos:pos-1]
38
+
39
+ # Parse individual skill entries
40
+ skills = []
41
+
42
+ # Split by skill entries (each starts with {)
43
+ skill_pattern = r'\{\s*"([^"]+)",\s*\{([^}]+)\},\s*\{([^}]+)\},\s*([^,\n]+),\s*([^,\n]+),\s*([^,\n]+),\s*([^,\n]+),\s*([^,\n]+),\s*([^,\n]+),\s*([^,\n]+),\s*"([^"]*)",\s*"([^"]*)"(?:,\s*"([^"]*)")?\s*\}'
44
+
45
+ for match in re.finditer(skill_pattern, table_content, re.MULTILINE | re.DOTALL):
46
+ name = match.group(1)
47
+ skill_levels = match.group(2).strip()
48
+ ratings = match.group(3).strip()
49
+ spell_fun = match.group(4).strip()
50
+ target = match.group(5).strip()
51
+ position = match.group(6).strip()
52
+ gsn = match.group(7).strip()
53
+ slot = match.group(8).strip()
54
+ min_mana = match.group(9).strip()
55
+ beats = match.group(10).strip()
56
+ noun_damage = match.group(11).strip()
57
+ msg_off = match.group(12).strip()
58
+ msg_obj = match.group(13).strip() if match.group(13) else ""
59
+
60
+ # Skip reserved entry
61
+ if name == "reserved":
62
+ continue
63
+
64
+ # Determine if this is a spell or skill
65
+ is_spell = "spell_" in spell_fun and spell_fun != "spell_null"
66
+ skill_type = "spell" if is_spell else "skill"
67
+
68
+ # Extract function name
69
+ function_name = name.replace(" ", "_")
70
+ if is_spell:
71
+ function_name = spell_fun.replace("spell_", "") if spell_fun.startswith("spell_") else function_name
72
+
73
+ # Parse mana cost
74
+ try:
75
+ mana_cost = int(min_mana) if min_mana.isdigit() else 0
76
+ except:
77
+ mana_cost = 0
78
+
79
+ # Parse lag (beats)
80
+ try:
81
+ lag = int(beats) if beats.isdigit() else 0
82
+ except:
83
+ lag = 0
84
+
85
+ # Determine target type
86
+ target_mapping = {
87
+ "TAR_IGNORE": "ignore",
88
+ "TAR_CHAR_OFFENSIVE": "victim",
89
+ "TAR_CHAR_DEFENSIVE": "friendly",
90
+ "TAR_CHAR_SELF": "self",
91
+ "TAR_OBJ_INV": "object",
92
+ "TAR_OBJ_CHAR_DEF": "character_or_object",
93
+ "TAR_OBJ_CHAR_OFF": "character_or_object"
94
+ }
95
+ target_type = target_mapping.get(target, "victim")
96
+
97
+ skill_entry = {
98
+ "name": name,
99
+ "type": skill_type,
100
+ "function": function_name,
101
+ "target": target_type,
102
+ "mana_cost": mana_cost,
103
+ "lag": lag,
104
+ "cooldown": 0, # Default cooldown
105
+ "failure_rate": 0.0, # Default failure rate
106
+ "messages": {
107
+ "damage": noun_damage,
108
+ "wear_off": msg_off
109
+ }
110
+ }
111
+
112
+ # Add object message if present
113
+ if msg_obj:
114
+ skill_entry["messages"]["object"] = msg_obj
115
+
116
+ skills.append(skill_entry)
117
+
118
+ return skills
119
+
120
+
121
+ def main():
122
+ """Convert ROM 2.4 skills to JSON."""
123
+ import argparse
124
+
125
+ parser = argparse.ArgumentParser(description="Convert ROM 2.4 skill_table to JSON")
126
+ parser.add_argument(
127
+ "--const-c",
128
+ type=Path,
129
+ default=Path("src/const.c"),
130
+ help="Path to const.c file (default: src/const.c)"
131
+ )
132
+ parser.add_argument(
133
+ "--output",
134
+ type=Path,
135
+ default=Path("data/skills.json"),
136
+ help="Output JSON file (default: data/skills.json)"
137
+ )
138
+
139
+ args = parser.parse_args()
140
+
141
+ if not args.const_c.exists():
142
+ print(f"Error: {args.const_c} not found")
143
+ return 1
144
+
145
+ try:
146
+ skills = parse_skill_table(args.const_c)
147
+
148
+ # Ensure output directory exists
149
+ args.output.parent.mkdir(parents=True, exist_ok=True)
150
+
151
+ # Write JSON with nice formatting
152
+ with args.output.open('w', encoding='utf-8') as f:
153
+ json.dump(skills, f, indent=2, ensure_ascii=False)
154
+
155
+ print(f"✅ Converted {len(skills)} skills to {args.output}")
156
+ print(f"Sample skills: {[s['name'] for s in skills[:5]]}")
157
+
158
+ return 0
159
+
160
+ except Exception as e:
161
+ print(f"Error converting skills: {e}")
162
+ return 1
163
+
164
+
165
+ if __name__ == "__main__":
166
+ exit(main())
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ """Convert ROM social.are to JSON matching schemas/social.schema.json.
4
+
5
+ Rules:
6
+ - Each social consists of a name line (optionally followed by two ints)
7
+ and up to 8 message lines in the order documented in doc/command.txt.
8
+ - A line consisting of a single "$" denotes an empty string for that field.
9
+ - A line consisting of a single "#" terminates the current social early;
10
+ remaining fields default to empty strings.
11
+ """
12
+
13
+ from pathlib import Path
14
+ import json
15
+ from typing import List, Dict
16
+
17
+ FIELDS = [
18
+ "char_no_arg",
19
+ "others_no_arg",
20
+ "char_found",
21
+ "others_found",
22
+ "vict_found",
23
+ "not_found",
24
+ "char_auto",
25
+ "others_auto",
26
+ ]
27
+
28
+
29
+ def parse_socials(text: str) -> List[Dict[str, str]]:
30
+ lines = [ln.rstrip("\n") for ln in text.splitlines()]
31
+ out: List[Dict[str, str]] = []
32
+ i = 0
33
+ # skip header
34
+ while i < len(lines) and not lines[i].startswith("#SOCIALS"):
35
+ i += 1
36
+ if i < len(lines) and lines[i].startswith("#SOCIALS"):
37
+ i += 1
38
+ # parse entries
39
+ while i < len(lines):
40
+ # skip blank lines
41
+ while i < len(lines) and lines[i].strip() == "":
42
+ i += 1
43
+ if i >= len(lines):
44
+ break
45
+ # name line or terminator
46
+ line = lines[i].strip()
47
+ if not line or line.startswith("#"):
48
+ i += 1
49
+ continue
50
+ # name may be followed by integers; take first token as name
51
+ name = line.split()[0]
52
+ i += 1
53
+ entry: Dict[str, str] = {"name": name}
54
+ # read up to 8 fields
55
+ remaining = len(FIELDS)
56
+ for field in FIELDS:
57
+ if i >= len(lines):
58
+ entry[field] = ""
59
+ continue
60
+ val = lines[i]
61
+ i += 1
62
+ if val == "#":
63
+ # early terminator: fill the rest with empty
64
+ entry[field] = ""
65
+ for rest in FIELDS[FIELDS.index(field) + 1 :]:
66
+ entry[rest] = ""
67
+ break
68
+ if val == "$":
69
+ entry[field] = ""
70
+ else:
71
+ entry[field] = val
72
+ remaining -= 1
73
+ out.append(entry)
74
+ return out
75
+
76
+
77
+ def convert(in_path: str | Path, out_path: str | Path) -> None:
78
+ text = Path(in_path).read_text(encoding="utf-8")
79
+ data = parse_socials(text)
80
+ Path(out_path).parent.mkdir(parents=True, exist_ok=True)
81
+ with open(out_path, "w", encoding="utf-8") as f:
82
+ json.dump(data, f, indent=2, ensure_ascii=False)
83
+
84
+
85
+ if __name__ == "__main__": # pragma: no cover
86
+ import argparse
87
+
88
+ ap = argparse.ArgumentParser(description="Convert ROM social.are to JSON")
89
+ ap.add_argument("infile", help="Path to area/social.are")
90
+ ap.add_argument("outfile", help="Output JSON path")
91
+ args = ap.parse_args()
92
+ convert(args.infile, args.outfile)
@@ -0,0 +1,17 @@
1
+ from mud.db.session import SessionLocal
2
+ from mud.db.models import PlayerAccount, Character
3
+ from mud.world.world_state import initialize_world
4
+
5
+
6
+ def load_test_user():
7
+ db = SessionLocal()
8
+
9
+ account = PlayerAccount(username="test", email="test@example.com")
10
+ account.set_password("test123")
11
+ db.add(account)
12
+ db.flush()
13
+
14
+ char = Character(name="Tester", hp=100, room_vnum=3001, player_id=account.id)
15
+ db.add(char)
16
+ db.commit()
17
+ print("✅ Test user created: login=test / pw=test123")
File without changes
mud/security/bans.py ADDED
@@ -0,0 +1,112 @@
1
+ """Simple ban registry for site/account bans (Phase 1).
2
+
3
+ This module provides in-memory helpers to enforce ROM-style bans at login.
4
+ Persistence and full ROM format will be added in a follow-up task.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Set
11
+
12
+ _banned_hosts: Set[str] = set()
13
+ _banned_accounts: Set[str] = set()
14
+
15
+ # Default storage location, mirroring ROM's BAN_FILE semantics.
16
+ BANS_FILE = Path("data/bans.txt")
17
+
18
+
19
+ def clear_all_bans() -> None:
20
+ _banned_hosts.clear()
21
+ _banned_accounts.clear()
22
+
23
+
24
+ def add_banned_host(host: str) -> None:
25
+ _banned_hosts.add(host.strip().lower())
26
+
27
+
28
+ def remove_banned_host(host: str) -> None:
29
+ _banned_hosts.discard(host.strip().lower())
30
+
31
+
32
+ def is_host_banned(host: str | None) -> bool:
33
+ if not host:
34
+ return False
35
+ return host.strip().lower() in _banned_hosts
36
+
37
+
38
+ def add_banned_account(username: str) -> None:
39
+ _banned_accounts.add(username.strip().lower())
40
+
41
+
42
+ def remove_banned_account(username: str) -> None:
43
+ _banned_accounts.discard(username.strip().lower())
44
+
45
+
46
+ def is_account_banned(username: str | None) -> bool:
47
+ if not username:
48
+ return False
49
+ return username.strip().lower() in _banned_accounts
50
+
51
+
52
+ # --- ROM-compatible persistence (minimal) ---
53
+
54
+ # ROM uses letter flags A.. for bit positions; for bans we need:
55
+ # BAN_ALL = D, BAN_PERMANENT = F. We emit "DF" for permanent site-wide bans.
56
+ _ROM_FLAG_ALL = "D"
57
+ _ROM_FLAG_PERM = "F"
58
+
59
+
60
+ def _flags_to_string() -> str:
61
+ # For now, we only persist permanent, all-site bans.
62
+ return _ROM_FLAG_ALL + _ROM_FLAG_PERM
63
+
64
+
65
+ def save_bans_file(path: Path | str | None = None) -> None:
66
+ """Write permanent site bans to file in ROM format.
67
+
68
+ Format per ROM src/ban.c save_bans():
69
+ "%-20s %-2d %s\n" → name, level, flags-as-letters
70
+ We don't track setter level yet; write level 0.
71
+ """
72
+ target = Path(path) if path else BANS_FILE
73
+ if not _banned_hosts:
74
+ # Mirror ROM behavior: delete file if no permanent bans remain.
75
+ try:
76
+ if target.exists():
77
+ target.unlink()
78
+ except OSError:
79
+ pass
80
+ return
81
+ target.parent.mkdir(parents=True, exist_ok=True)
82
+ with target.open("w", encoding="utf-8") as fp:
83
+ for host in sorted(_banned_hosts):
84
+ name = host
85
+ level = 0
86
+ flags = _flags_to_string()
87
+ fp.write(f"{name:<20} {level:2d} {flags}\n")
88
+
89
+
90
+ def load_bans_file(path: Path | str | None = None) -> int:
91
+ """Load bans from ROM-format file into memory; returns count loaded."""
92
+ target = Path(path) if path else BANS_FILE
93
+ if not target.exists():
94
+ return 0
95
+ count = 0
96
+ with target.open("r", encoding="utf-8") as fp:
97
+ for raw in fp:
98
+ line = raw.strip()
99
+ if not line:
100
+ continue
101
+ # Expect: name(<20 padded>) <level> <flags>
102
+ parts = line.split()
103
+ if len(parts) < 3:
104
+ continue
105
+ name = parts[0]
106
+ # level = parts[1] # unused here
107
+ flags = parts[2]
108
+ # Only import entries that include permanent+all flags
109
+ if _ROM_FLAG_PERM in flags:
110
+ _banned_hosts.add(name.lower())
111
+ count += 1
112
+ return count
@@ -0,0 +1,20 @@
1
+ import hashlib
2
+ import os
3
+
4
+
5
+ def hash_password(password: str) -> str:
6
+ """Return a salted hash for the given password."""
7
+ salt = os.urandom(16)
8
+ hashed = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 100_000)
9
+ return salt.hex() + ":" + hashed.hex()
10
+
11
+
12
+ def verify_password(password: str, stored_hash: str) -> bool:
13
+ """Check password against the stored ``salt:hash`` string."""
14
+ try:
15
+ salt_hex, hash_hex = stored_hash.split(":")
16
+ except ValueError:
17
+ return False
18
+ salt = bytes.fromhex(salt_hex)
19
+ new_hash = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 100_000)
20
+ return new_hash.hex() == hash_hex
mud/server.py ADDED
@@ -0,0 +1,8 @@
1
+ import asyncio
2
+ from mud.net.telnet_server import start_server
3
+ from mud.config import HOST, PORT
4
+
5
+
6
+ def run_game_loop():
7
+ print("\U0001F30D Starting MUD server...")
8
+ asyncio.run(start_server(host=HOST, port=PORT))
mud/skills/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .registry import SkillRegistry, load_skills, skill_registry
2
+
3
+ __all__ = ["SkillRegistry", "load_skills", "skill_registry"]