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.
- mud/__init__.py +0 -0
- mud/__main__.py +40 -0
- mud/account/__init__.py +20 -0
- mud/account/account_manager.py +62 -0
- mud/account/account_service.py +80 -0
- mud/advancement.py +62 -0
- mud/affects/saves.py +123 -0
- mud/agent/__init__.py +0 -0
- mud/agent/agent_protocol.py +19 -0
- mud/agent/character_agent.py +61 -0
- mud/combat/__init__.py +3 -0
- mud/combat/engine.py +189 -0
- mud/commands/__init__.py +3 -0
- mud/commands/admin_commands.py +77 -0
- mud/commands/advancement.py +36 -0
- mud/commands/alias_cmds.py +44 -0
- mud/commands/build.py +18 -0
- mud/commands/combat.py +16 -0
- mud/commands/communication.py +55 -0
- mud/commands/decorators.py +11 -0
- mud/commands/dispatcher.py +206 -0
- mud/commands/healer.py +81 -0
- mud/commands/help.py +14 -0
- mud/commands/imc.py +19 -0
- mud/commands/inspection.py +113 -0
- mud/commands/inventory.py +42 -0
- mud/commands/movement.py +71 -0
- mud/commands/notes.py +44 -0
- mud/commands/shop.py +138 -0
- mud/commands/socials.py +34 -0
- mud/config.py +59 -0
- mud/db/__init__.py +0 -0
- mud/db/init.py +27 -0
- mud/db/migrate_from_files.py +87 -0
- mud/db/migrations.py +7 -0
- mud/db/models.py +98 -0
- mud/db/seed.py +28 -0
- mud/db/session.py +11 -0
- mud/devtools/__init__.py +0 -0
- mud/devtools/agent_demo.py +19 -0
- mud/entrypoint.py +34 -0
- mud/game_loop.py +117 -0
- mud/imc/__init__.py +17 -0
- mud/imc/protocol.py +32 -0
- mud/loaders/__init__.py +27 -0
- mud/loaders/area_loader.py +73 -0
- mud/loaders/base_loader.py +38 -0
- mud/loaders/help_loader.py +17 -0
- mud/loaders/json_area_loader.py +203 -0
- mud/loaders/json_loader.py +285 -0
- mud/loaders/mob_loader.py +104 -0
- mud/loaders/obj_loader.py +76 -0
- mud/loaders/reset_loader.py +29 -0
- mud/loaders/room_loader.py +63 -0
- mud/loaders/shop_loader.py +41 -0
- mud/loaders/social_loader.py +16 -0
- mud/loaders/specials_loader.py +63 -0
- mud/logging/__init__.py +0 -0
- mud/logging/admin.py +40 -0
- mud/logging/agent_trace.py +9 -0
- mud/math/c_compat.py +27 -0
- mud/mobprog.py +72 -0
- mud/models/__init__.py +106 -0
- mud/models/area.py +33 -0
- mud/models/area_json.py +27 -0
- mud/models/board.py +49 -0
- mud/models/board_json.py +16 -0
- mud/models/character.py +195 -0
- mud/models/character_json.py +46 -0
- mud/models/constants.py +423 -0
- mud/models/conversion.py +45 -0
- mud/models/help.py +28 -0
- mud/models/help_json.py +14 -0
- mud/models/json_io.py +64 -0
- mud/models/mob.py +82 -0
- mud/models/note.py +29 -0
- mud/models/note_json.py +16 -0
- mud/models/obj.py +82 -0
- mud/models/object.py +28 -0
- mud/models/object_json.py +40 -0
- mud/models/player_json.py +29 -0
- mud/models/room.py +86 -0
- mud/models/room_json.py +46 -0
- mud/models/shop.py +21 -0
- mud/models/shop_json.py +17 -0
- mud/models/skill.py +25 -0
- mud/models/skill_json.py +20 -0
- mud/models/social.py +78 -0
- mud/models/social_json.py +20 -0
- mud/net/__init__.py +9 -0
- mud/net/ansi.py +27 -0
- mud/net/connection.py +174 -0
- mud/net/protocol.py +57 -0
- mud/net/session.py +21 -0
- mud/net/telnet_server.py +31 -0
- mud/network/__init__.py +0 -0
- mud/network/websocket_server.py +83 -0
- mud/network/websocket_session.py +21 -0
- mud/notes.py +45 -0
- mud/persistence.py +185 -0
- mud/registry.py +5 -0
- mud/scripts/convert_are_to_json.py +162 -0
- mud/scripts/convert_help_are_to_json.py +92 -0
- mud/scripts/convert_player_to_json.py +112 -0
- mud/scripts/convert_shops_to_json.py +64 -0
- mud/scripts/convert_skills_to_json.py +166 -0
- mud/scripts/convert_social_are_to_json.py +92 -0
- mud/scripts/load_test_data.py +17 -0
- mud/security/__init__.py +0 -0
- mud/security/bans.py +112 -0
- mud/security/hash_utils.py +20 -0
- mud/server.py +8 -0
- mud/skills/__init__.py +3 -0
- mud/skills/handlers.py +795 -0
- mud/skills/registry.py +97 -0
- mud/spawning/__init__.py +1 -0
- mud/spawning/mob_spawner.py +13 -0
- mud/spawning/obj_spawner.py +18 -0
- mud/spawning/reset_handler.py +222 -0
- mud/spawning/templates.py +63 -0
- mud/spec_funs.py +57 -0
- mud/time.py +48 -0
- mud/utils/rng_mm.py +123 -0
- mud/wiznet.py +74 -0
- mud/world/__init__.py +11 -0
- mud/world/linking.py +31 -0
- mud/world/look.py +29 -0
- mud/world/movement.py +135 -0
- mud/world/world_state.py +179 -0
- rom24_quickmud_python-1.2.2.dist-info/METADATA +236 -0
- rom24_quickmud_python-1.2.2.dist-info/RECORD +135 -0
- rom24_quickmud_python-1.2.2.dist-info/WHEEL +5 -0
- rom24_quickmud_python-1.2.2.dist-info/entry_points.txt +2 -0
- rom24_quickmud_python-1.2.2.dist-info/licenses/LICENSE +21 -0
- 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")
|
mud/security/__init__.py
ADDED
|
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
mud/skills/__init__.py
ADDED