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/wiznet.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Wiznet flags and helpers.
|
|
2
|
+
|
|
3
|
+
Provides flag definitions and broadcast filtering for immortal channels.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from enum import IntFlag
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WiznetFlag(IntFlag):
|
|
12
|
+
"""Wiznet flags mirroring ROM bit values."""
|
|
13
|
+
|
|
14
|
+
WIZ_ON = 0x00000001
|
|
15
|
+
WIZ_TICKS = 0x00000002
|
|
16
|
+
WIZ_LOGINS = 0x00000004
|
|
17
|
+
WIZ_SITES = 0x00000008
|
|
18
|
+
WIZ_LINKS = 0x00000010
|
|
19
|
+
WIZ_DEATHS = 0x00000020
|
|
20
|
+
WIZ_RESETS = 0x00000040
|
|
21
|
+
WIZ_MOBDEATHS = 0x00000080
|
|
22
|
+
WIZ_FLAGS = 0x00000100
|
|
23
|
+
WIZ_PENALTIES = 0x00000200
|
|
24
|
+
WIZ_SACCING = 0x00000400
|
|
25
|
+
WIZ_LEVELS = 0x00000800
|
|
26
|
+
WIZ_SECURE = 0x00001000
|
|
27
|
+
WIZ_SWITCHES = 0x00002000
|
|
28
|
+
WIZ_SNOOPS = 0x00004000
|
|
29
|
+
WIZ_RESTORE = 0x00008000
|
|
30
|
+
WIZ_LOAD = 0x00010000
|
|
31
|
+
WIZ_NEWBIE = 0x00020000
|
|
32
|
+
WIZ_SPAM = 0x00040000
|
|
33
|
+
WIZ_DEBUG = 0x00080000
|
|
34
|
+
WIZ_MEMORY = 0x00100000
|
|
35
|
+
WIZ_SKILLS = 0x00200000
|
|
36
|
+
WIZ_TESTING = 0x00400000
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING: # pragma: no cover - for type hints only
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def wiznet(message: str, flag: WiznetFlag) -> None:
|
|
44
|
+
"""Broadcast *message* to immortals subscribed to *flag*.
|
|
45
|
+
|
|
46
|
+
Immortals must have WIZ_ON and the given *flag* set to receive the message.
|
|
47
|
+
"""
|
|
48
|
+
from mud.models.character import character_registry
|
|
49
|
+
|
|
50
|
+
for ch in list(character_registry):
|
|
51
|
+
if not getattr(ch, "is_admin", False):
|
|
52
|
+
continue
|
|
53
|
+
if not getattr(ch, "wiznet", 0) & WiznetFlag.WIZ_ON:
|
|
54
|
+
continue
|
|
55
|
+
if not getattr(ch, "wiznet", 0) & flag:
|
|
56
|
+
continue
|
|
57
|
+
if hasattr(ch, "messages"):
|
|
58
|
+
ch.messages.append(message)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def cmd_wiznet(char, args: str) -> str:
|
|
62
|
+
"""Toggle WIZ_ON for immortal *char*.
|
|
63
|
+
|
|
64
|
+
Only immortals may use this command. With no arguments it flips the
|
|
65
|
+
:class:`WiznetFlag.WIZ_ON` bit and reports the new state.
|
|
66
|
+
"""
|
|
67
|
+
from mud.models.character import Character # local import to avoid cycle
|
|
68
|
+
|
|
69
|
+
if not isinstance(char, Character) or not getattr(char, "is_admin", False):
|
|
70
|
+
return "Huh?"
|
|
71
|
+
|
|
72
|
+
char.wiznet ^= int(WiznetFlag.WIZ_ON)
|
|
73
|
+
state = "on" if char.wiznet & int(WiznetFlag.WIZ_ON) else "off"
|
|
74
|
+
return f"Wiznet is now {state}."
|
mud/world/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .world_state import initialize_world, create_test_character, fix_all_exits
|
|
2
|
+
from .movement import move_character
|
|
3
|
+
from .look import look
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"initialize_world",
|
|
7
|
+
"create_test_character",
|
|
8
|
+
"fix_all_exits",
|
|
9
|
+
"move_character",
|
|
10
|
+
"look",
|
|
11
|
+
]
|
mud/world/linking.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import logging
|
|
3
|
+
from mud.registry import room_registry
|
|
4
|
+
from mud.models.constants import Direction
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def link_exits() -> None:
|
|
8
|
+
"""Replace exit vnum references with actual Room objects."""
|
|
9
|
+
for room in room_registry.values():
|
|
10
|
+
for idx, exit in enumerate(room.exits):
|
|
11
|
+
if exit is None:
|
|
12
|
+
continue
|
|
13
|
+
if exit.to_room is not None:
|
|
14
|
+
continue
|
|
15
|
+
if exit.vnum <= 0:
|
|
16
|
+
# Negative or zero vnums denote an intentionally unset exit.
|
|
17
|
+
# These should be ignored rather than reported as errors.
|
|
18
|
+
continue
|
|
19
|
+
target = room_registry.get(exit.vnum)
|
|
20
|
+
if target:
|
|
21
|
+
exit.to_room = target
|
|
22
|
+
else:
|
|
23
|
+
logging.warning(
|
|
24
|
+
"Unlinked exit in room %s -> %s (target %s not found)",
|
|
25
|
+
room.vnum,
|
|
26
|
+
Direction(idx).name.lower(),
|
|
27
|
+
exit.vnum,
|
|
28
|
+
)
|
|
29
|
+
if not hasattr(room, "unlinked_exits"):
|
|
30
|
+
room.unlinked_exits = set()
|
|
31
|
+
room.unlinked_exits.add(Direction(idx))
|
mud/world/look.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from mud.models.character import Character
|
|
3
|
+
from mud.models.constants import Direction
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
dir_names = {
|
|
7
|
+
Direction.NORTH: "north",
|
|
8
|
+
Direction.EAST: "east",
|
|
9
|
+
Direction.SOUTH: "south",
|
|
10
|
+
Direction.WEST: "west",
|
|
11
|
+
Direction.UP: "up",
|
|
12
|
+
Direction.DOWN: "down",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def look(char: Character) -> str:
|
|
17
|
+
room = char.room
|
|
18
|
+
if not room:
|
|
19
|
+
return "You are floating in a void..."
|
|
20
|
+
exit_list = [dir_names[Direction(i)] for i, ex in enumerate(room.exits) if ex]
|
|
21
|
+
lines = [room.name or "", room.description or ""]
|
|
22
|
+
if exit_list:
|
|
23
|
+
lines.append(f"[Exits: {' '.join(exit_list)}]")
|
|
24
|
+
if room.contents:
|
|
25
|
+
lines.append("Objects: " + ", ".join(obj.short_descr or obj.name or "object" for obj in room.contents))
|
|
26
|
+
others = [p.name or "someone" for p in room.people if p is not char]
|
|
27
|
+
if others:
|
|
28
|
+
lines.append("Characters: " + ", ".join(others))
|
|
29
|
+
return "\n".join(lines).strip()
|
mud/world/movement.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Dict, Iterable
|
|
3
|
+
|
|
4
|
+
from mud.models.character import Character
|
|
5
|
+
from mud.models.constants import Direction, Sector, AffectFlag, ItemType
|
|
6
|
+
from mud.net.protocol import broadcast_room
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
dir_map: Dict[str, Direction] = {
|
|
10
|
+
"north": Direction.NORTH,
|
|
11
|
+
"east": Direction.EAST,
|
|
12
|
+
"south": Direction.SOUTH,
|
|
13
|
+
"west": Direction.WEST,
|
|
14
|
+
"up": Direction.UP,
|
|
15
|
+
"down": Direction.DOWN,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ROM str_app carry table (carry column only) for STR 0..25.
|
|
20
|
+
# Source: src/const.c:str_app (third field), multiplied by 10 in handler.c.
|
|
21
|
+
_STR_CARRY = [
|
|
22
|
+
0, # 0
|
|
23
|
+
3, 3, 10, 25, 55, # 1..5
|
|
24
|
+
80, 90, 100, 100, 115, 115, 130, 130, 140, 150, 165, 180, 200, 225, 250, 300, 350, 400, 450, 500,
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_curr_stat(ch: Character, idx: int) -> int | None:
|
|
29
|
+
stats = getattr(ch, "perm_stat", None) or []
|
|
30
|
+
if idx < len(stats) and stats[idx] > 0:
|
|
31
|
+
val = stats[idx]
|
|
32
|
+
return max(0, min(25, int(val)))
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def can_carry_w(ch: Character) -> int:
|
|
37
|
+
"""Carry weight capacity.
|
|
38
|
+
|
|
39
|
+
- If STR stat present: use ROM formula `str_app[STR].carry * 10 + level * 25`.
|
|
40
|
+
- Otherwise: preserve prior fixed cap (100) to avoid changing existing tests.
|
|
41
|
+
"""
|
|
42
|
+
s = _get_curr_stat(ch, 0) # STAT_STR
|
|
43
|
+
if s is None:
|
|
44
|
+
return 100
|
|
45
|
+
carry = _STR_CARRY[s]
|
|
46
|
+
return carry * 10 + ch.level * 25
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def can_carry_n(ch: Character) -> int:
|
|
50
|
+
"""Carry number capacity.
|
|
51
|
+
|
|
52
|
+
- If DEX stat present: use ROM-like `MAX_WEAR + 2*DEX + level` (MAX_WEAR≈19).
|
|
53
|
+
- Otherwise: preserve prior fixed cap (30).
|
|
54
|
+
"""
|
|
55
|
+
d = _get_curr_stat(ch, 1) # STAT_DEX
|
|
56
|
+
if d is None:
|
|
57
|
+
return 30
|
|
58
|
+
MAX_WEAR = 19
|
|
59
|
+
return MAX_WEAR + 2 * d + ch.level
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def move_character(char: Character, direction: str) -> str:
|
|
63
|
+
dir_key = direction.lower()
|
|
64
|
+
if dir_key not in dir_map:
|
|
65
|
+
return "You cannot go that way."
|
|
66
|
+
|
|
67
|
+
if char.carry_weight > can_carry_w(char) or char.carry_number > can_carry_n(char):
|
|
68
|
+
return "You are too encumbered to move."
|
|
69
|
+
|
|
70
|
+
idx = dir_map[dir_key]
|
|
71
|
+
exit = char.room.exits[idx]
|
|
72
|
+
if exit is None or exit.to_room is None:
|
|
73
|
+
return "You cannot go that way."
|
|
74
|
+
|
|
75
|
+
current_room = char.room
|
|
76
|
+
target_room = exit.to_room
|
|
77
|
+
|
|
78
|
+
# --- Sector-based gating and movement costs (ROM act_move.c) ---
|
|
79
|
+
from_sector = Sector(current_room.sector_type)
|
|
80
|
+
to_sector = Sector(target_room.sector_type)
|
|
81
|
+
|
|
82
|
+
# Air requires flying unless immortal/admin
|
|
83
|
+
if (from_sector == Sector.AIR or to_sector == Sector.AIR):
|
|
84
|
+
if not char.is_admin and not bool(char.affected_by & AffectFlag.FLYING):
|
|
85
|
+
return "You can't fly."
|
|
86
|
+
|
|
87
|
+
# Water (no swim) requires a boat unless flying or immortal
|
|
88
|
+
if (from_sector == Sector.WATER_NOSWIM or to_sector == Sector.WATER_NOSWIM):
|
|
89
|
+
if not char.is_admin and not bool(char.affected_by & AffectFlag.FLYING):
|
|
90
|
+
def has_boat(objs: Iterable):
|
|
91
|
+
for o in objs:
|
|
92
|
+
proto = getattr(o, "prototype", None)
|
|
93
|
+
if proto and getattr(proto, "item_type", None) == int(ItemType.BOAT):
|
|
94
|
+
return True
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
has_boat_item = has_boat(char.inventory) or has_boat(getattr(char, "equipment", {}).values())
|
|
98
|
+
if not has_boat_item:
|
|
99
|
+
return "You need a boat to go there."
|
|
100
|
+
|
|
101
|
+
movement_loss = {
|
|
102
|
+
Sector.INSIDE: 1,
|
|
103
|
+
Sector.CITY: 2,
|
|
104
|
+
Sector.FIELD: 2,
|
|
105
|
+
Sector.FOREST: 3,
|
|
106
|
+
Sector.HILLS: 4,
|
|
107
|
+
Sector.MOUNTAIN: 6,
|
|
108
|
+
Sector.WATER_SWIM: 4,
|
|
109
|
+
Sector.WATER_NOSWIM: 1,
|
|
110
|
+
Sector.UNUSED: 6,
|
|
111
|
+
Sector.AIR: 10,
|
|
112
|
+
Sector.DESERT: 6,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
move_cost = (movement_loss.get(from_sector, 2) + movement_loss.get(to_sector, 2)) // 2
|
|
116
|
+
# Conditional effects
|
|
117
|
+
if char.affected_by & AffectFlag.FLYING or char.affected_by & AffectFlag.HASTE:
|
|
118
|
+
move_cost = max(0, move_cost // 2)
|
|
119
|
+
if char.affected_by & AffectFlag.SLOW:
|
|
120
|
+
move_cost *= 2
|
|
121
|
+
|
|
122
|
+
if char.move < move_cost:
|
|
123
|
+
return "You are too exhausted."
|
|
124
|
+
|
|
125
|
+
# Apply short wait-state and deduct movement points
|
|
126
|
+
char.wait = max(char.wait, 1)
|
|
127
|
+
char.move -= move_cost
|
|
128
|
+
|
|
129
|
+
broadcast_room(current_room, f"{char.name} leaves {dir_key}.", exclude=char)
|
|
130
|
+
if char in current_room.people:
|
|
131
|
+
current_room.people.remove(char)
|
|
132
|
+
target_room.people.append(char)
|
|
133
|
+
char.room = target_room
|
|
134
|
+
broadcast_room(target_room, f"{char.name} arrives.", exclude=char)
|
|
135
|
+
return f"You walk {dir_key} to {target_room.name}."
|
mud/world/world_state.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from mud.loaders import load_all_areas
|
|
3
|
+
from mud.loaders.json_area_loader import load_all_areas_from_json
|
|
4
|
+
from mud.loaders.json_loader import load_all_areas_from_json as load_enhanced_json
|
|
5
|
+
from mud.registry import room_registry, area_registry, mob_registry, obj_registry
|
|
6
|
+
from mud.db.session import SessionLocal
|
|
7
|
+
from mud.db import models
|
|
8
|
+
from mud.models.character import Character, character_registry
|
|
9
|
+
from mud.models.constants import Position
|
|
10
|
+
from mud.spawning.reset_handler import apply_resets
|
|
11
|
+
from .linking import link_exits
|
|
12
|
+
from mud.security import bans
|
|
13
|
+
|
|
14
|
+
# Global skill registry for world initialization
|
|
15
|
+
skill_registry = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_world_from_db() -> bool:
|
|
19
|
+
"""Populate registries from the database."""
|
|
20
|
+
session = SessionLocal()
|
|
21
|
+
|
|
22
|
+
db_rooms = session.query(models.Room).all()
|
|
23
|
+
for db_room in db_rooms:
|
|
24
|
+
room = models_to_room(db_room)
|
|
25
|
+
room_registry[room.vnum] = room
|
|
26
|
+
|
|
27
|
+
db_exits = session.query(models.Exit).all()
|
|
28
|
+
for db_exit in db_exits:
|
|
29
|
+
origin_room = session.query(models.Room).get(db_exit.room_id)
|
|
30
|
+
source = room_registry.get(origin_room.vnum) if origin_room else None
|
|
31
|
+
target = room_registry.get(db_exit.to_room_vnum)
|
|
32
|
+
if source and target:
|
|
33
|
+
if len(source.exits) <= int(db_exit.direction):
|
|
34
|
+
source.exits.extend([None] * (int(db_exit.direction) - len(source.exits) + 1))
|
|
35
|
+
source.exits[int(db_exit.direction)] = target
|
|
36
|
+
|
|
37
|
+
for db_mob in session.query(models.MobPrototype).all():
|
|
38
|
+
mob_registry[db_mob.vnum] = models_to_mob(db_mob)
|
|
39
|
+
|
|
40
|
+
for db_obj in session.query(models.ObjPrototype).all():
|
|
41
|
+
obj_registry[db_obj.vnum] = models_to_obj(db_obj)
|
|
42
|
+
|
|
43
|
+
print(
|
|
44
|
+
f"\u2705 Loaded {len(room_registry)} rooms, {len(mob_registry)} mobs, {len(obj_registry)} objects."
|
|
45
|
+
)
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def models_to_room(db_room: models.Room):
|
|
50
|
+
from mud.models.room import Room
|
|
51
|
+
|
|
52
|
+
return Room(
|
|
53
|
+
vnum=db_room.vnum,
|
|
54
|
+
name=db_room.name,
|
|
55
|
+
description=db_room.description,
|
|
56
|
+
sector_type=db_room.sector_type or 0,
|
|
57
|
+
room_flags=db_room.room_flags or 0,
|
|
58
|
+
exits=[None] * 10,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def models_to_mob(db_mob: models.MobPrototype):
|
|
63
|
+
from mud.models.mob import MobIndex
|
|
64
|
+
|
|
65
|
+
return MobIndex(
|
|
66
|
+
vnum=db_mob.vnum,
|
|
67
|
+
player_name=db_mob.name,
|
|
68
|
+
short_descr=db_mob.short_desc,
|
|
69
|
+
long_descr=db_mob.long_desc,
|
|
70
|
+
level=db_mob.level or 0,
|
|
71
|
+
alignment=db_mob.alignment or 0,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def models_to_obj(db_obj: models.ObjPrototype):
|
|
76
|
+
from mud.models.obj import ObjIndex
|
|
77
|
+
|
|
78
|
+
return ObjIndex(
|
|
79
|
+
vnum=db_obj.vnum,
|
|
80
|
+
name=db_obj.name,
|
|
81
|
+
short_descr=db_obj.short_desc,
|
|
82
|
+
description=db_obj.long_desc,
|
|
83
|
+
item_type=db_obj.item_type or 0,
|
|
84
|
+
extra_flags=db_obj.flags or 0,
|
|
85
|
+
value=[db_obj.value0, db_obj.value1, db_obj.value2, db_obj.value3],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def initialize_world(area_list_path: str | None = "area/area.lst", use_json: bool = True) -> None:
|
|
90
|
+
"""Initialize world from files or database.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
area_list_path: Path to area.lst file (for legacy .are loading)
|
|
94
|
+
use_json: If True, load from JSON files in data/areas/. If False, use legacy .are files.
|
|
95
|
+
"""
|
|
96
|
+
# Tiny fix: ensure a clean ban registry at boot and between tests.
|
|
97
|
+
# ROM loads bans from disk at boot; tests may add bans in-memory.
|
|
98
|
+
# Clearing here avoids leakage across test modules without affecting
|
|
99
|
+
# persistence tests which explicitly save/load.
|
|
100
|
+
bans.clear_all_bans()
|
|
101
|
+
|
|
102
|
+
# Load skills registry from JSON
|
|
103
|
+
from mud.skills.registry import SkillRegistry
|
|
104
|
+
from pathlib import Path
|
|
105
|
+
skills_path = Path("data/skills.json")
|
|
106
|
+
if skills_path.exists():
|
|
107
|
+
try:
|
|
108
|
+
global skill_registry
|
|
109
|
+
skill_registry = SkillRegistry()
|
|
110
|
+
skill_registry.load(skills_path)
|
|
111
|
+
print(f"✅ Loaded {len(skill_registry.skills)} skills from {skills_path}")
|
|
112
|
+
except Exception as e:
|
|
113
|
+
print(f"Warning: Failed to load skills from {skills_path}: {e}")
|
|
114
|
+
skill_registry = None
|
|
115
|
+
|
|
116
|
+
# Load shops from JSON (needed for shopkeeper detection in resets)
|
|
117
|
+
from mud.registry import shop_registry
|
|
118
|
+
from mud.loaders.shop_loader import Shop
|
|
119
|
+
import json
|
|
120
|
+
shops_path = Path("data/shops.json")
|
|
121
|
+
if shops_path.exists():
|
|
122
|
+
try:
|
|
123
|
+
with open(shops_path, 'r') as f:
|
|
124
|
+
shops_data = json.load(f)
|
|
125
|
+
shop_registry.clear()
|
|
126
|
+
for shop_data in shops_data:
|
|
127
|
+
# Convert string buy_types back to int list for compatibility
|
|
128
|
+
buy_types = []
|
|
129
|
+
for bt in shop_data.get('buy_types', []):
|
|
130
|
+
if isinstance(bt, str):
|
|
131
|
+
from mud.models.constants import ItemType
|
|
132
|
+
try:
|
|
133
|
+
buy_types.append(ItemType[bt.upper()].value)
|
|
134
|
+
except (KeyError, AttributeError):
|
|
135
|
+
buy_types.append(0) # unknown type
|
|
136
|
+
else:
|
|
137
|
+
buy_types.append(bt)
|
|
138
|
+
|
|
139
|
+
shop_registry[shop_data['keeper']] = Shop(
|
|
140
|
+
keeper=shop_data['keeper'],
|
|
141
|
+
buy_types=buy_types,
|
|
142
|
+
profit_buy=shop_data.get('profit_buy', 100),
|
|
143
|
+
profit_sell=shop_data.get('profit_sell', 100),
|
|
144
|
+
open_hour=shop_data.get('open_hour', 0),
|
|
145
|
+
close_hour=shop_data.get('close_hour', 23),
|
|
146
|
+
)
|
|
147
|
+
print(f"✅ Loaded {len(shop_registry)} shops from {shops_path}")
|
|
148
|
+
except Exception as e:
|
|
149
|
+
print(f"Warning: Failed to load shops from {shops_path}: {e}")
|
|
150
|
+
|
|
151
|
+
if area_list_path:
|
|
152
|
+
if use_json:
|
|
153
|
+
# Load from JSON files using enhanced field mapping
|
|
154
|
+
from mud.loaders.json_loader import load_all_areas_from_json
|
|
155
|
+
json_areas = load_all_areas_from_json("data/areas")
|
|
156
|
+
# Areas are already registered in area_registry by the JSON loader
|
|
157
|
+
else:
|
|
158
|
+
# Load from legacy .are files
|
|
159
|
+
load_all_areas(area_list_path)
|
|
160
|
+
link_exits()
|
|
161
|
+
for area in area_registry.values():
|
|
162
|
+
apply_resets(area)
|
|
163
|
+
else:
|
|
164
|
+
load_world_from_db()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def fix_all_exits() -> None:
|
|
168
|
+
link_exits()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def create_test_character(name: str, room_vnum: int) -> Character:
|
|
172
|
+
room = room_registry.get(room_vnum)
|
|
173
|
+
char = Character(name=name)
|
|
174
|
+
# ROM default: new players start standing.
|
|
175
|
+
char.position = int(Position.STANDING)
|
|
176
|
+
if room:
|
|
177
|
+
room.add_character(char)
|
|
178
|
+
character_registry.append(char)
|
|
179
|
+
return char
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rom24-quickmud-python
|
|
3
|
+
Version: 1.2.2
|
|
4
|
+
Summary: A modern Python port of the ROM 2.4b6 MUD engine with full telnet server and JSON world loading
|
|
5
|
+
Author-email: Mark Jedrzejczyk <mark.jedrzejczyk@gmail.com>
|
|
6
|
+
Maintainer-email: Mark Jedrzejczyk <mark.jedrzejczyk@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/Nostoi/rom24-quickmud-python
|
|
9
|
+
Project-URL: Repository, https://github.com/Nostoi/rom24-quickmud-python.git
|
|
10
|
+
Project-URL: Documentation, https://github.com/Nostoi/rom24-quickmud-python/blob/master/README.md
|
|
11
|
+
Project-URL: Bug Tracker, https://github.com/Nostoi/rom24-quickmud-python/issues
|
|
12
|
+
Project-URL: Changelog, https://github.com/Nostoi/rom24-quickmud-python/releases
|
|
13
|
+
Keywords: mud,rom,game,mmo,telnet,server
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Environment :: No Input/Output (Daemon)
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Topic :: Games/Entertainment :: Multi-User Dungeons (MUD)
|
|
25
|
+
Classifier: Topic :: Internet
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: SQLAlchemy<3,>=2.0
|
|
32
|
+
Requires-Dist: typer>=0.9
|
|
33
|
+
Requires-Dist: python-dotenv>=1.0
|
|
34
|
+
Requires-Dist: fastapi>=0.68.0
|
|
35
|
+
Requires-Dist: uvicorn>=0.15.0
|
|
36
|
+
Requires-Dist: pytest-timeout>=2.1.0
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
39
|
+
Requires-Dist: pytest-cov>=6.0; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest-timeout>=2.1.0; extra == "dev"
|
|
41
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
42
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
43
|
+
Requires-Dist: black>=22.0; extra == "dev"
|
|
44
|
+
Requires-Dist: jsonschema>=4.0; extra == "dev"
|
|
45
|
+
Requires-Dist: build>=0.10.0; extra == "dev"
|
|
46
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
47
|
+
Provides-Extra: test
|
|
48
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
49
|
+
Requires-Dist: pytest-cov>=6.0; extra == "test"
|
|
50
|
+
Requires-Dist: pytest-timeout>=2.1.0; extra == "test"
|
|
51
|
+
Dynamic: license-file
|
|
52
|
+
|
|
53
|
+
# QuickMUD - A Modern ROM 2.4 Python Port
|
|
54
|
+
|
|
55
|
+
[](https://badge.fury.io/py/quickmud)
|
|
56
|
+
[](https://www.python.org/downloads/)
|
|
57
|
+
[](https://opensource.org/licenses/MIT)
|
|
58
|
+
[](https://github.com/Nostoi/rom24-quickmud-python)
|
|
59
|
+
|
|
60
|
+
**QuickMUD is a modern Python port of the legendary ROM 2.4b6 MUD engine**, derived from ROM 2.4b6, Merc 2.1 and DikuMUD. This is a complete rewrite that brings the classic text-based MMORPG experience to modern Python with async networking, JSON world data, and comprehensive testing.
|
|
61
|
+
|
|
62
|
+
## 🎮 What is a MUD?
|
|
63
|
+
|
|
64
|
+
A "[Multi-User Dungeon](https://en.wikipedia.org/wiki/MUD)" (MUD) is a text-based MMORPG that runs over telnet. ROM is renowned for its fast-paced combat system and rich player interaction. ROM was also the foundation for [Carrion Fields](http://www.carrionfields.net/), one of the most acclaimed MUDs ever created.
|
|
65
|
+
|
|
66
|
+
## ✨ Key Features
|
|
67
|
+
|
|
68
|
+
- **🚀 Modern Python Architecture**: Fully async/await networking with SQLAlchemy ORM
|
|
69
|
+
- **📡 Multi-User Telnet Server**: Handle hundreds of concurrent players
|
|
70
|
+
- **🗺️ JSON World Loading**: Easy-to-edit world data with 352+ room resets
|
|
71
|
+
- **🏪 Complete Shop System**: Buy, sell, and list items with working economy
|
|
72
|
+
- **⚔️ ROM Combat System**: Classic ROM combat mechanics and skill system
|
|
73
|
+
- **👥 Social Features**: Say, tell, shout, and 100+ social interactions
|
|
74
|
+
- **🛠️ Admin Commands**: Teleport, spawn, ban management, and OLC building
|
|
75
|
+
- **📊 100% Test Coverage**: 200+ tests ensure reliability and stability
|
|
76
|
+
|
|
77
|
+
## 📦 Installation
|
|
78
|
+
|
|
79
|
+
### For Players & Server Operators
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install quickmud
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Quick Start
|
|
86
|
+
|
|
87
|
+
Run a QuickMUD server:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
mud runserver
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The server will start on `localhost:4000`. Connect with any telnet client:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
telnet localhost 4000
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 🏗️ For Developers
|
|
100
|
+
|
|
101
|
+
## 🏗️ For Developers
|
|
102
|
+
|
|
103
|
+
### Development Installation
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
git clone https://github.com/Nostoi/rom24-quickmud-python.git
|
|
107
|
+
cd rom24-quickmud-python
|
|
108
|
+
python -m venv venv
|
|
109
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
110
|
+
pip install -e .[dev]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Running Tests
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pytest # Run all 200 tests (should complete in ~16 seconds)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Development Server
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
python -m mud # Start development server
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## 🎯 Project Status
|
|
126
|
+
|
|
127
|
+
- **Version**: 1.2.0 (Production Ready)
|
|
128
|
+
- **Test Coverage**: 200/200 tests passing (100% success rate)
|
|
129
|
+
- **Performance**: Full test suite completes in ~16 seconds
|
|
130
|
+
- **Compatibility**: Python 3.10+, cross-platform
|
|
131
|
+
|
|
132
|
+
## 🏛️ Architecture
|
|
133
|
+
|
|
134
|
+
- **Async Networking**: Modern async/await telnet server
|
|
135
|
+
- **SQLAlchemy ORM**: Robust database layer with migrations
|
|
136
|
+
- **JSON World Data**: Human-readable area files with full ROM compatibility
|
|
137
|
+
- **Modular Design**: Clean separation of concerns (commands, world, networking)
|
|
138
|
+
- **Type Safety**: Comprehensive type hints throughout codebase
|
|
139
|
+
|
|
140
|
+
## 📜 License
|
|
141
|
+
|
|
142
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
143
|
+
|
|
144
|
+
## 🤝 Contributing
|
|
145
|
+
|
|
146
|
+
Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md) and feel free to submit pull requests.
|
|
147
|
+
|
|
148
|
+
## 📚 Documentation
|
|
149
|
+
|
|
150
|
+
- [Installation Guide](docs/installation.md)
|
|
151
|
+
- [Configuration](docs/configuration.md)
|
|
152
|
+
- [World Building](docs/world-building.md)
|
|
153
|
+
- [API Reference](docs/api.md)
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
**Experience the classic MUD gameplay with modern Python reliability!** 🐍✨
|
|
158
|
+
|
|
159
|
+
For a fully reproducible environment, use the pinned requirements files generated with [pip-tools](https://github.com/jazzband/pip-tools):
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
pip install -r requirements-dev.txt
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
To update the pinned dependencies:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
pip-compile requirements.in
|
|
169
|
+
pip-compile requirements-dev.in
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Tools like [Poetry](https://python-poetry.org/) provide a similar workflow if you prefer that approach.
|
|
173
|
+
|
|
174
|
+
Run tests with:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
pytest
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Publishing
|
|
181
|
+
|
|
182
|
+
To release a new version to PyPI:
|
|
183
|
+
|
|
184
|
+
1. Update the version in `pyproject.toml`.
|
|
185
|
+
2. Commit and tag:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
git commit -am "release: v1.2.3"
|
|
189
|
+
git tag v1.2.3
|
|
190
|
+
git push origin main --tags
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The GitHub Actions workflow will build and publish the package when the tag is pushed.
|
|
194
|
+
|
|
195
|
+
## Python Architecture
|
|
196
|
+
|
|
197
|
+
Game systems are implemented in Python modules:
|
|
198
|
+
|
|
199
|
+
- `mud/net` provides asynchronous telnet and websocket servers.
|
|
200
|
+
- `mud/game_loop.py` drives the tick-based update loop.
|
|
201
|
+
- `mud/commands` contains the command dispatcher and handlers.
|
|
202
|
+
- `mud/combat` and `mud/skills` implement combat and abilities.
|
|
203
|
+
- `mud/persistence.py` handles saving characters and world state.
|
|
204
|
+
|
|
205
|
+
Start the server with:
|
|
206
|
+
|
|
207
|
+
```sh
|
|
208
|
+
python -m mud runserver
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Docker Image
|
|
212
|
+
|
|
213
|
+
Build and run the Python server with Docker:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
docker build -t quickmud .
|
|
217
|
+
docker run -p 5000:5000 quickmud
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Or use docker-compose to rebuild on changes and mount the repository:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
docker-compose up
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Connect via:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
telnet localhost 5000
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Data Models
|
|
233
|
+
|
|
234
|
+
The `mud/models` package defines dataclasses used by the game engine.
|
|
235
|
+
They mirror the JSON schemas in `schemas/` and supply enums and registries
|
|
236
|
+
for loading and manipulating area, room, object, and character data.
|