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
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,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()
|