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/models/social.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .social_json import SocialJson
|
|
6
|
+
from mud.models.constants import Sex
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Social:
|
|
11
|
+
"""Runtime representation of a social command."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
char_no_arg: str = ""
|
|
15
|
+
others_no_arg: str = ""
|
|
16
|
+
char_found: str = ""
|
|
17
|
+
others_found: str = ""
|
|
18
|
+
vict_found: str = ""
|
|
19
|
+
not_found: str = ""
|
|
20
|
+
char_auto: str = ""
|
|
21
|
+
others_auto: str = ""
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_json(cls, data: SocialJson) -> "Social":
|
|
25
|
+
return cls(**data.to_dict())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# placeholder registry to track loaded socials
|
|
29
|
+
social_registry: dict[str, Social] = {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def register_social(social: Social) -> None:
|
|
33
|
+
"""Register a social by its lowercase name."""
|
|
34
|
+
social_registry[social.name.lower()] = social
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# START socials
|
|
38
|
+
def expand_placeholders(message: str, actor: object, victim: object | None = None) -> str:
|
|
39
|
+
"""Replace basic ROM placeholders in social messages."""
|
|
40
|
+
def subj(sex: Sex | object) -> str:
|
|
41
|
+
if isinstance(sex, Sex):
|
|
42
|
+
return {Sex.MALE: "he", Sex.FEMALE: "she", Sex.NONE: "it"}.get(sex, "they")
|
|
43
|
+
return "they"
|
|
44
|
+
|
|
45
|
+
def obj(sex: Sex | object) -> str:
|
|
46
|
+
if isinstance(sex, Sex):
|
|
47
|
+
return {Sex.MALE: "him", Sex.FEMALE: "her", Sex.NONE: "it"}.get(sex, "them")
|
|
48
|
+
return "them"
|
|
49
|
+
|
|
50
|
+
def poss(sex: Sex | object) -> str:
|
|
51
|
+
if isinstance(sex, Sex):
|
|
52
|
+
return {Sex.MALE: "his", Sex.FEMALE: "her", Sex.NONE: "its"}.get(sex, "their")
|
|
53
|
+
return "their"
|
|
54
|
+
|
|
55
|
+
# Names
|
|
56
|
+
result = message.replace("$n", getattr(actor, "name", ""))
|
|
57
|
+
if victim is not None:
|
|
58
|
+
result = result.replace("$N", getattr(victim, "name", ""))
|
|
59
|
+
|
|
60
|
+
# Actor pronouns: replace $mself before $m to avoid overlap
|
|
61
|
+
asex = getattr(actor, "sex", None)
|
|
62
|
+
result = result.replace(
|
|
63
|
+
"$mself",
|
|
64
|
+
{Sex.MALE: "himself", Sex.FEMALE: "herself", Sex.NONE: "itself"}.get(asex, "themselves"),
|
|
65
|
+
)
|
|
66
|
+
result = result.replace("$e", subj(asex))
|
|
67
|
+
result = result.replace("$m", obj(asex))
|
|
68
|
+
result = result.replace("$s", poss(asex))
|
|
69
|
+
|
|
70
|
+
# Victim pronouns: $E $M $S
|
|
71
|
+
if victim is not None:
|
|
72
|
+
vsex = getattr(victim, "sex", None)
|
|
73
|
+
result = result.replace("$E", subj(vsex))
|
|
74
|
+
result = result.replace("$M", obj(vsex))
|
|
75
|
+
result = result.replace("$S", poss(vsex))
|
|
76
|
+
|
|
77
|
+
return result
|
|
78
|
+
# END socials
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .json_io import JsonDataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class SocialJson(JsonDataclass):
|
|
10
|
+
"""Social command messages matching ``schemas/social.schema.json``."""
|
|
11
|
+
|
|
12
|
+
name: str
|
|
13
|
+
char_no_arg: str = ""
|
|
14
|
+
others_no_arg: str = ""
|
|
15
|
+
char_found: str = ""
|
|
16
|
+
others_found: str = ""
|
|
17
|
+
vict_found: str = ""
|
|
18
|
+
not_found: str = ""
|
|
19
|
+
char_auto: str = ""
|
|
20
|
+
others_auto: str = ""
|
mud/net/__init__.py
ADDED
mud/net/ansi.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""ANSI color code translation for ROM-style tokens."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
ANSI_CODES: dict[str, str] = {
|
|
5
|
+
"{x": "\x1b[0m",
|
|
6
|
+
"{r": "\x1b[31m",
|
|
7
|
+
"{g": "\x1b[32m",
|
|
8
|
+
"{y": "\x1b[33m",
|
|
9
|
+
"{b": "\x1b[34m",
|
|
10
|
+
"{m": "\x1b[35m",
|
|
11
|
+
"{c": "\x1b[36m",
|
|
12
|
+
"{w": "\x1b[37m",
|
|
13
|
+
"{R": "\x1b[1;31m",
|
|
14
|
+
"{G": "\x1b[1;32m",
|
|
15
|
+
"{Y": "\x1b[1;33m",
|
|
16
|
+
"{B": "\x1b[1;34m",
|
|
17
|
+
"{M": "\x1b[1;35m",
|
|
18
|
+
"{C": "\x1b[1;36m",
|
|
19
|
+
"{W": "\x1b[1;37m",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def translate_ansi(text: str) -> str:
|
|
24
|
+
"""Replace ROM color tokens with ANSI escape sequences."""
|
|
25
|
+
for token, code in ANSI_CODES.items():
|
|
26
|
+
text = text.replace(token, code)
|
|
27
|
+
return text
|
mud/net/connection.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
from mud.account import (
|
|
5
|
+
load_character,
|
|
6
|
+
save_character,
|
|
7
|
+
create_account,
|
|
8
|
+
login_with_host,
|
|
9
|
+
list_characters,
|
|
10
|
+
create_character,
|
|
11
|
+
)
|
|
12
|
+
from mud.commands import process_command
|
|
13
|
+
from mud.net.session import Session, SESSIONS
|
|
14
|
+
from mud.net.protocol import send_to_char
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def handle_connection(
|
|
18
|
+
reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
|
19
|
+
) -> None:
|
|
20
|
+
addr = writer.get_extra_info("peername")
|
|
21
|
+
host_for_ban = None
|
|
22
|
+
if isinstance(addr, tuple) and addr:
|
|
23
|
+
host_for_ban = addr[0]
|
|
24
|
+
session = None
|
|
25
|
+
char = None
|
|
26
|
+
account = None
|
|
27
|
+
username = ""
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
writer.write(b"Welcome to PythonMUD\r\n")
|
|
31
|
+
await writer.drain()
|
|
32
|
+
|
|
33
|
+
# Account login / creation
|
|
34
|
+
while not account:
|
|
35
|
+
writer.write(b"Account: ")
|
|
36
|
+
await writer.drain()
|
|
37
|
+
name_data = await reader.readline()
|
|
38
|
+
if not name_data:
|
|
39
|
+
return
|
|
40
|
+
username = name_data.decode().strip()
|
|
41
|
+
writer.write(b"Password: ")
|
|
42
|
+
await writer.drain()
|
|
43
|
+
pwd_data = await reader.readline()
|
|
44
|
+
if not pwd_data:
|
|
45
|
+
return
|
|
46
|
+
password = pwd_data.decode().strip()
|
|
47
|
+
# Enforce site/account bans at login time
|
|
48
|
+
account = login_with_host(username, password, host_for_ban)
|
|
49
|
+
if not account:
|
|
50
|
+
if create_account(username, password):
|
|
51
|
+
account = login_with_host(username, password, host_for_ban)
|
|
52
|
+
else:
|
|
53
|
+
writer.write(b"Login failed.\r\n")
|
|
54
|
+
await writer.drain()
|
|
55
|
+
|
|
56
|
+
# Character selection / creation
|
|
57
|
+
chars = list_characters(account)
|
|
58
|
+
if chars:
|
|
59
|
+
writer.write(
|
|
60
|
+
("Characters: " + ", ".join(chars) + "\r\n").encode()
|
|
61
|
+
)
|
|
62
|
+
writer.write(b"Character: ")
|
|
63
|
+
await writer.drain()
|
|
64
|
+
char_data = await reader.readline()
|
|
65
|
+
if not char_data:
|
|
66
|
+
return
|
|
67
|
+
char_name = char_data.decode().strip()
|
|
68
|
+
if char_name not in chars:
|
|
69
|
+
create_character(account, char_name)
|
|
70
|
+
try:
|
|
71
|
+
char = load_character(username, char_name)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
print(f"[ERROR] Failed to load character {char_name}: {e}")
|
|
74
|
+
return
|
|
75
|
+
if char and char.room:
|
|
76
|
+
try:
|
|
77
|
+
char.room.add_character(char)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
print(f"[ERROR] Failed to add character to room: {e}")
|
|
80
|
+
|
|
81
|
+
char.connection = writer
|
|
82
|
+
|
|
83
|
+
session = Session(
|
|
84
|
+
name=char.name or "",
|
|
85
|
+
character=char,
|
|
86
|
+
reader=reader,
|
|
87
|
+
writer=writer,
|
|
88
|
+
)
|
|
89
|
+
SESSIONS[session.name] = session
|
|
90
|
+
print(f"[CONNECT] {addr} as {session.name}")
|
|
91
|
+
|
|
92
|
+
# Send initial room description and prompt
|
|
93
|
+
try:
|
|
94
|
+
if char and char.room:
|
|
95
|
+
response = process_command(char, "look")
|
|
96
|
+
await send_to_char(char, response)
|
|
97
|
+
else:
|
|
98
|
+
await send_to_char(char, "You are floating in a void...")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
print(f"[ERROR] Failed to send initial look: {e}")
|
|
101
|
+
await send_to_char(char, "Welcome to the world!")
|
|
102
|
+
|
|
103
|
+
# Main command loop with error handling
|
|
104
|
+
while True:
|
|
105
|
+
try:
|
|
106
|
+
writer.write(b"> ")
|
|
107
|
+
await writer.drain()
|
|
108
|
+
data = await reader.readline()
|
|
109
|
+
if not data:
|
|
110
|
+
break
|
|
111
|
+
command = data.decode().strip()
|
|
112
|
+
if not command:
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
response = process_command(char, command)
|
|
117
|
+
await send_to_char(char, response)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
print(
|
|
120
|
+
"[ERROR] Command processing failed for "
|
|
121
|
+
f"'{command}': {e}"
|
|
122
|
+
)
|
|
123
|
+
await send_to_char(
|
|
124
|
+
char,
|
|
125
|
+
"Sorry, there was an error processing that "
|
|
126
|
+
"command.",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# flush broadcast messages queued on character
|
|
130
|
+
while char and char.messages:
|
|
131
|
+
try:
|
|
132
|
+
msg = char.messages.pop(0)
|
|
133
|
+
await send_to_char(char, msg)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
print(f"[ERROR] Failed to send message: {e}")
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
except asyncio.CancelledError:
|
|
139
|
+
break
|
|
140
|
+
except Exception as e:
|
|
141
|
+
print(
|
|
142
|
+
"[ERROR] Connection loop error for "
|
|
143
|
+
f"{session.name if session else 'unknown'}: {e}"
|
|
144
|
+
)
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
print(f"[ERROR] Connection handler error for {addr}: {e}")
|
|
149
|
+
finally:
|
|
150
|
+
# Cleanup with error handling
|
|
151
|
+
try:
|
|
152
|
+
if char:
|
|
153
|
+
save_character(char)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
print(f"[ERROR] Failed to save character: {e}")
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
if char and char.room:
|
|
159
|
+
char.room.remove_character(char)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
print(f"[ERROR] Failed to remove character from room: {e}")
|
|
162
|
+
|
|
163
|
+
if session and session.name in SESSIONS:
|
|
164
|
+
SESSIONS.pop(session.name, None)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
writer.close()
|
|
168
|
+
await writer.wait_closed()
|
|
169
|
+
except Exception as e:
|
|
170
|
+
print(f"[ERROR] Failed to close connection: {e}")
|
|
171
|
+
|
|
172
|
+
print(
|
|
173
|
+
f"[DISCONNECT] {addr} as {session.name if session else 'unknown'}"
|
|
174
|
+
)
|
mud/net/protocol.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import asyncio
|
|
3
|
+
from typing import Iterable, Optional
|
|
4
|
+
|
|
5
|
+
from mud.models.character import Character, character_registry
|
|
6
|
+
from mud.net.ansi import translate_ansi
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def send_to_char(char: Character, message: str | Iterable[str]) -> None:
|
|
10
|
+
"""Send message to character's connection with CRLF."""
|
|
11
|
+
writer = getattr(char, "connection", None)
|
|
12
|
+
if writer is None:
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
if isinstance(message, (list, tuple)):
|
|
16
|
+
text = "\r\n".join(str(m) for m in message)
|
|
17
|
+
else:
|
|
18
|
+
text = str(message)
|
|
19
|
+
text = translate_ansi(text)
|
|
20
|
+
|
|
21
|
+
if not text.endswith("\r\n"):
|
|
22
|
+
text += "\r\n"
|
|
23
|
+
writer.write(text.encode())
|
|
24
|
+
await writer.drain()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def broadcast_room(
|
|
28
|
+
room,
|
|
29
|
+
message: str,
|
|
30
|
+
exclude: Optional[Character] = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
for char in list(getattr(room, "people", [])):
|
|
33
|
+
if char is exclude:
|
|
34
|
+
continue
|
|
35
|
+
writer = getattr(char, "connection", None)
|
|
36
|
+
if writer:
|
|
37
|
+
# fire and forget
|
|
38
|
+
asyncio.create_task(send_to_char(char, message))
|
|
39
|
+
if hasattr(char, "messages"):
|
|
40
|
+
char.messages.append(message)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def broadcast_global(
|
|
44
|
+
message: str,
|
|
45
|
+
channel: str,
|
|
46
|
+
exclude: Optional[Character] = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
for char in list(character_registry):
|
|
49
|
+
if char is exclude:
|
|
50
|
+
continue
|
|
51
|
+
if channel in getattr(char, "muted_channels", set()):
|
|
52
|
+
continue
|
|
53
|
+
writer = getattr(char, "connection", None)
|
|
54
|
+
if writer:
|
|
55
|
+
asyncio.create_task(send_to_char(char, message))
|
|
56
|
+
if hasattr(char, "messages"):
|
|
57
|
+
char.messages.append(message)
|
mud/net/session.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Dict
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
from mud.models.character import Character
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Session:
|
|
11
|
+
name: str
|
|
12
|
+
character: Character
|
|
13
|
+
reader: asyncio.StreamReader
|
|
14
|
+
writer: asyncio.StreamWriter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
SESSIONS: Dict[str, Session] = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_online_players() -> list[Character]:
|
|
21
|
+
return [sess.character for sess in SESSIONS.values()]
|
mud/net/telnet_server.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
from mud.world.world_state import initialize_world
|
|
5
|
+
from mud.db.migrations import run_migrations
|
|
6
|
+
from .connection import handle_connection
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def create_server(
|
|
10
|
+
host: str = "0.0.0.0", port: int = 4000, area_list: str = "area/area.lst"
|
|
11
|
+
) -> asyncio.AbstractServer:
|
|
12
|
+
"""Return a started telnet server without blocking the loop."""
|
|
13
|
+
# Initialize database tables
|
|
14
|
+
run_migrations()
|
|
15
|
+
# Initialize world data
|
|
16
|
+
initialize_world(area_list)
|
|
17
|
+
return await asyncio.start_server(handle_connection, host, port)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def start_server(
|
|
21
|
+
host: str = "0.0.0.0", port: int = 4000, area_list: str = "area/area.lst"
|
|
22
|
+
) -> None:
|
|
23
|
+
server = await create_server(host, port, area_list)
|
|
24
|
+
addr = server.sockets[0].getsockname()
|
|
25
|
+
print(f"Serving on {addr}")
|
|
26
|
+
async with server:
|
|
27
|
+
await server.serve_forever()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
asyncio.run(start_server())
|
mud/network/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
3
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
4
|
+
import uvicorn
|
|
5
|
+
|
|
6
|
+
from mud.config import HOST, PORT, CORS_ORIGINS
|
|
7
|
+
from mud.world.world_state import initialize_world, create_test_character
|
|
8
|
+
from mud.account import load_character, save_character
|
|
9
|
+
from mud.commands import process_command
|
|
10
|
+
from .websocket_session import WebSocketPlayerSession
|
|
11
|
+
|
|
12
|
+
app = FastAPI()
|
|
13
|
+
|
|
14
|
+
app.add_middleware(
|
|
15
|
+
CORSMiddleware,
|
|
16
|
+
allow_origins=CORS_ORIGINS,
|
|
17
|
+
allow_credentials=True,
|
|
18
|
+
allow_methods=["*"],
|
|
19
|
+
allow_headers=["*"],
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.on_event("startup")
|
|
24
|
+
async def startup() -> None:
|
|
25
|
+
initialize_world(None)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.websocket("/ws")
|
|
29
|
+
async def websocket_endpoint(websocket: WebSocket):
|
|
30
|
+
await websocket.accept()
|
|
31
|
+
await websocket.send_json({"type": "info", "text": "Welcome to PythonMUD. What is your name?"})
|
|
32
|
+
try:
|
|
33
|
+
data = await websocket.receive_json()
|
|
34
|
+
except WebSocketDisconnect:
|
|
35
|
+
return
|
|
36
|
+
name = data.get("text", "guest")
|
|
37
|
+
char = load_character(name, name)
|
|
38
|
+
if not char:
|
|
39
|
+
char = create_test_character(name, 3001)
|
|
40
|
+
elif char.room:
|
|
41
|
+
char.room.add_character(char)
|
|
42
|
+
|
|
43
|
+
session = WebSocketPlayerSession(websocket=websocket, character=char, name=name)
|
|
44
|
+
char.connection = session
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
while True:
|
|
48
|
+
try:
|
|
49
|
+
message = await session.recv()
|
|
50
|
+
except WebSocketDisconnect:
|
|
51
|
+
break
|
|
52
|
+
if message.get("type") != "command":
|
|
53
|
+
continue
|
|
54
|
+
command = message.get("text", "").strip()
|
|
55
|
+
if not command:
|
|
56
|
+
continue
|
|
57
|
+
response = process_command(char, command)
|
|
58
|
+
await session.send({
|
|
59
|
+
"type": "output",
|
|
60
|
+
"text": response,
|
|
61
|
+
"room": char.room.vnum if getattr(char, "room", None) else None,
|
|
62
|
+
"hp": char.hit,
|
|
63
|
+
})
|
|
64
|
+
while char.messages:
|
|
65
|
+
msg = char.messages.pop(0)
|
|
66
|
+
await session.send({
|
|
67
|
+
"type": "output",
|
|
68
|
+
"text": msg,
|
|
69
|
+
"room": char.room.vnum if getattr(char, "room", None) else None,
|
|
70
|
+
"hp": char.hit,
|
|
71
|
+
})
|
|
72
|
+
finally:
|
|
73
|
+
save_character(char)
|
|
74
|
+
if char.room:
|
|
75
|
+
char.room.remove_character(char)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def run(host: str = HOST, port: int = PORT) -> None:
|
|
79
|
+
uvicorn.run("mud.network.websocket_server:app", host=host, port=port)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
run()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
from fastapi import WebSocket
|
|
5
|
+
from mud.models.character import Character
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class WebSocketPlayerSession:
|
|
10
|
+
"""Session wrapper for WebSocket clients."""
|
|
11
|
+
|
|
12
|
+
websocket: WebSocket
|
|
13
|
+
character: Character
|
|
14
|
+
name: str
|
|
15
|
+
session_type: str = "websocket"
|
|
16
|
+
|
|
17
|
+
async def send(self, payload: Dict[str, Any]) -> None:
|
|
18
|
+
await self.websocket.send_json(payload)
|
|
19
|
+
|
|
20
|
+
async def recv(self) -> Dict[str, Any]:
|
|
21
|
+
return await self.websocket.receive_json()
|
mud/notes.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from mud.models.board import Board
|
|
8
|
+
from mud.models.board_json import BoardJson
|
|
9
|
+
from mud.models.json_io import load_dataclass, dump_dataclass
|
|
10
|
+
|
|
11
|
+
BOARDS_DIR = Path("data/boards")
|
|
12
|
+
|
|
13
|
+
board_registry: Dict[str, Board] = {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_boards() -> None:
|
|
17
|
+
"""Load all boards from ``BOARDS_DIR`` into ``board_registry``."""
|
|
18
|
+
board_registry.clear()
|
|
19
|
+
if not BOARDS_DIR.exists():
|
|
20
|
+
return
|
|
21
|
+
for path in BOARDS_DIR.glob("*.json"):
|
|
22
|
+
with path.open() as f:
|
|
23
|
+
data = load_dataclass(BoardJson, f)
|
|
24
|
+
board_registry[data.name] = Board.from_json(data)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def save_board(board: Board) -> None:
|
|
28
|
+
"""Persist ``board`` to ``BOARDS_DIR`` atomically."""
|
|
29
|
+
BOARDS_DIR.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
path = BOARDS_DIR / f"{board.name}.json"
|
|
31
|
+
tmp = path.with_suffix(".tmp")
|
|
32
|
+
with tmp.open("w") as f:
|
|
33
|
+
dump_dataclass(board.to_json(), f, indent=2)
|
|
34
|
+
f.flush()
|
|
35
|
+
os.fsync(f.fileno())
|
|
36
|
+
os.replace(tmp, path)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_board(name: str, description: str | None = None) -> Board:
|
|
40
|
+
"""Fetch a board by name, creating it if necessary."""
|
|
41
|
+
board = board_registry.get(name)
|
|
42
|
+
if not board:
|
|
43
|
+
board = Board(name=name, description=description or name.title())
|
|
44
|
+
board_registry[name] = board
|
|
45
|
+
return board
|