rom24-quickmud-python 2.7.0__py3-none-any.whl → 2.9.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/account/account_manager.py +301 -75
- mud/account/account_service.py +23 -18
- mud/advancement.py +8 -2
- mud/affects/engine.py +54 -0
- mud/ai/__init__.py +9 -65
- mud/combat/death.py +3 -1
- mud/combat/engine.py +209 -76
- mud/combat/messages.py +67 -31
- mud/commands/admin_commands.py +4 -1
- mud/commands/advancement.py +14 -8
- mud/commands/auto_settings.py +32 -3
- mud/commands/build.py +1184 -95
- mud/commands/character.py +2 -4
- mud/commands/combat.py +108 -59
- mud/commands/communication.py +108 -18
- mud/commands/dispatcher.py +92 -18
- mud/commands/imm_admin.py +8 -0
- mud/commands/imm_emote.py +8 -3
- mud/commands/imm_olc.py +292 -63
- mud/commands/info.py +10 -6
- mud/commands/info_extended.py +88 -51
- mud/commands/inventory.py +5 -2
- mud/commands/misc_info.py +155 -146
- mud/commands/notes.py +42 -1
- mud/commands/remaining_rom.py +44 -6
- mud/commands/session.py +36 -33
- mud/db/migrations.py +66 -0
- mud/db/models.py +48 -1
- mud/db/serializers.py +622 -0
- mud/game_loop.py +295 -65
- mud/groups/xp.py +30 -30
- mud/handler.py +28 -26
- mud/loaders/base_loader.py +2 -4
- mud/loaders/json_loader.py +14 -6
- mud/loaders/obj_loader.py +15 -10
- mud/loaders/room_loader.py +21 -2
- mud/mob_cmds.py +3 -9
- mud/models/__init__.py +1 -2
- mud/models/board.py +1 -0
- mud/models/character.py +194 -19
- mud/models/mob.py +25 -0
- mud/models/obj.py +11 -49
- mud/models/object.py +63 -2
- mud/models/room.py +14 -1
- mud/models/titles.py +292 -0
- mud/models/weapon_table.py +86 -0
- mud/music/__init__.py +73 -12
- mud/net/connection.py +363 -98
- mud/network/websocket_server.py +21 -13
- mud/network/websocket_stream.py +5 -1
- mud/rom_api.py +18 -5
- mud/scripts/convert_are_to_json.py +1 -1
- mud/skills/handlers.py +51 -57
- mud/skills/registry.py +4 -7
- mud/spawning/obj_spawner.py +6 -1
- mud/spawning/templates.py +0 -1
- mud/utils/act.py +9 -5
- mud/utils/prompt.py +96 -6
- mud/wiznet.py +17 -0
- mud/world/char_find.py +4 -0
- mud/world/look.py +18 -29
- mud/world/time_persistence.py +61 -0
- mud/world/vision.py +31 -0
- mud/world/world_state.py +9 -0
- {rom24_quickmud_python-2.7.0.dist-info → rom24_quickmud_python-2.9.2.dist-info}/METADATA +30 -41
- {rom24_quickmud_python-2.7.0.dist-info → rom24_quickmud_python-2.9.2.dist-info}/RECORD +70 -67
- mud/persistence.py +0 -1239
- {rom24_quickmud_python-2.7.0.dist-info → rom24_quickmud_python-2.9.2.dist-info}/WHEEL +0 -0
- {rom24_quickmud_python-2.7.0.dist-info → rom24_quickmud_python-2.9.2.dist-info}/entry_points.txt +0 -0
- {rom24_quickmud_python-2.7.0.dist-info → rom24_quickmud_python-2.9.2.dist-info}/licenses/LICENSE +0 -0
- {rom24_quickmud_python-2.7.0.dist-info → rom24_quickmud_python-2.9.2.dist-info}/top_level.txt +0 -0
mud/account/account_manager.py
CHANGED
|
@@ -1,34 +1,75 @@
|
|
|
1
|
+
"""Account manager — DB-canonical persistence (INV-008 Phase 2).
|
|
2
|
+
|
|
3
|
+
The DB row (mud.db.models.Character) is the single source of truth for ALL
|
|
4
|
+
player gameplay state. The JSON pfile path (mud.persistence) has been removed.
|
|
5
|
+
|
|
6
|
+
``save_character`` writes all 71 fields to the DB row via ``save_character_to_db``.
|
|
7
|
+
``load_character`` queries the DB row and returns a fully-initialized runtime
|
|
8
|
+
Character via ``from_orm``.
|
|
9
|
+
|
|
10
|
+
ROM Reference: src/save.c fread_char / fwrite_char — the C engine's pfile is
|
|
11
|
+
the single source of truth; the only external auth store is the pfile itself.
|
|
12
|
+
Python equivalent: the DB row owns both auth and all gameplay state.
|
|
13
|
+
|
|
14
|
+
INV-003: ``character_registry`` membership is maintained — ``load_character``
|
|
15
|
+
appends to the registry after a successful DB load.
|
|
16
|
+
|
|
17
|
+
INV-008 (DB-CANONICAL): The invariant has been reversed — the DB row is now
|
|
18
|
+
the canonical source, not the JSON pfile. There is no JSON fallback path.
|
|
19
|
+
"""
|
|
20
|
+
|
|
1
21
|
from __future__ import annotations
|
|
2
22
|
|
|
3
|
-
import
|
|
23
|
+
import time
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
4
25
|
|
|
5
26
|
from mud.db.models import Character as DBCharacter
|
|
6
27
|
from mud.db.session import SessionLocal
|
|
7
|
-
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from sqlalchemy.orm import Session
|
|
31
|
+
from mud.models.character import Character, character_registry, from_orm
|
|
8
32
|
from mud.models.constants import ROOM_VNUM_LIMBO, ROOM_VNUM_TEMPLE
|
|
9
|
-
from mud.
|
|
10
|
-
|
|
11
|
-
|
|
33
|
+
from mud.db.serializers import (
|
|
34
|
+
_normalize_int_list,
|
|
35
|
+
_serialize_colour_table,
|
|
36
|
+
_serialize_object,
|
|
37
|
+
_serialize_pet,
|
|
38
|
+
_serialize_skill_map,
|
|
39
|
+
_serialize_groups,
|
|
12
40
|
)
|
|
13
41
|
|
|
14
42
|
|
|
15
43
|
def load_character(char_name: str, _ignored: str | None = None) -> Character | None:
|
|
16
|
-
"""Load a character by name from the
|
|
44
|
+
"""Load a character by name from the DB row (DB-canonical path).
|
|
17
45
|
|
|
18
46
|
The second argument is accepted but ignored for backward compatibility;
|
|
19
|
-
previously it was a username (account name). Characters are
|
|
20
|
-
|
|
47
|
+
previously it was a username (account name). Characters are standalone
|
|
48
|
+
identities — mirroring ROM src/save.c:fread_char.
|
|
49
|
+
|
|
50
|
+
The loaded character is appended to ``character_registry`` (INV-003).
|
|
21
51
|
"""
|
|
22
52
|
session = None
|
|
23
53
|
try:
|
|
24
54
|
session = SessionLocal()
|
|
25
55
|
db_char = session.query(DBCharacter).filter(DBCharacter.name == char_name).first()
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
56
|
+
if db_char is None:
|
|
57
|
+
return None
|
|
58
|
+
char = from_orm(db_char)
|
|
59
|
+
if char is not None:
|
|
60
|
+
# INV-009 REGISTRY-DISCONNECT-CLEANUP / dedup-by-name:
|
|
61
|
+
# ROM only ever has one player object by a given name in
|
|
62
|
+
# char_list. Drop any prior Character with the same name
|
|
63
|
+
# (e.g. the level=0 bare-row load during nanny name/password
|
|
64
|
+
# phase) before appending the freshly-loaded one. This
|
|
65
|
+
# complements INV-003 and prevents in-session duplicates
|
|
66
|
+
# surfacing through the promote-from-bare-row path.
|
|
67
|
+
for prior in [c for c in character_registry if getattr(c, "name", None) == char.name]:
|
|
68
|
+
character_registry.remove(prior)
|
|
69
|
+
character_registry.append(char) # INV-003
|
|
29
70
|
return char
|
|
30
71
|
except Exception as e:
|
|
31
|
-
print(f"[ERROR] Failed to load character {char_name}: {e}")
|
|
72
|
+
print(f"[ERROR] Failed to load character {char_name} from DB: {e}")
|
|
32
73
|
return None
|
|
33
74
|
finally:
|
|
34
75
|
if session:
|
|
@@ -36,73 +77,258 @@ def load_character(char_name: str, _ignored: str | None = None) -> Character | N
|
|
|
36
77
|
|
|
37
78
|
|
|
38
79
|
def save_character(character: Character) -> None:
|
|
80
|
+
"""Persist ``character`` to the DB row (DB-canonical path).
|
|
81
|
+
|
|
82
|
+
UPDATE-only: the character row must already exist (created via
|
|
83
|
+
account_service.create_character). If the row is not found, returns silently.
|
|
84
|
+
|
|
85
|
+
ROM Reference: src/save.c fwrite_char — all fields persisted.
|
|
86
|
+
# mirroring mud/persistence.py:save_character (now removed for JSON path)
|
|
87
|
+
"""
|
|
39
88
|
session = None
|
|
40
89
|
try:
|
|
41
90
|
session = SessionLocal()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
print(f"[WARN] Character '{character.name}' not found in database, creating new record")
|
|
45
|
-
db_char = DBCharacter(name=character.name)
|
|
46
|
-
session.add(db_char)
|
|
47
|
-
|
|
48
|
-
if db_char:
|
|
49
|
-
db_char.level = character.level
|
|
50
|
-
db_char.hp = character.hit
|
|
51
|
-
db_char.race = int(character.race or 0)
|
|
52
|
-
db_char.ch_class = int(character.ch_class or 0)
|
|
53
|
-
pcdata = getattr(character, "pcdata", None)
|
|
54
|
-
true_sex_value = int(getattr(pcdata, "true_sex", getattr(character, "sex", 0)) or 0)
|
|
55
|
-
db_char.true_sex = true_sex_value
|
|
56
|
-
db_char.sex = int(character.sex or true_sex_value or 0)
|
|
57
|
-
db_char.alignment = int(character.alignment or 0)
|
|
58
|
-
db_char.act = int(getattr(character, "act", 0) or 0)
|
|
59
|
-
db_char.hometown_vnum = int(character.hometown_vnum or 0)
|
|
60
|
-
db_char.perm_stats = json.dumps([int(val) for val in character.perm_stat])
|
|
61
|
-
db_char.size = int(character.size or 0)
|
|
62
|
-
db_char.form = int(character.form or 0)
|
|
63
|
-
db_char.parts = int(character.parts or 0)
|
|
64
|
-
db_char.imm_flags = int(character.imm_flags or 0)
|
|
65
|
-
db_char.res_flags = int(character.res_flags or 0)
|
|
66
|
-
db_char.vuln_flags = int(character.vuln_flags or 0)
|
|
67
|
-
db_char.practice = int(character.practice or 0)
|
|
68
|
-
db_char.train = int(character.train or 0)
|
|
69
|
-
|
|
70
|
-
# Save perm stats from pcdata (ROM src/handler.c stores perm_hit/perm_mana/perm_move)
|
|
71
|
-
if pcdata:
|
|
72
|
-
db_char.perm_hit = int(getattr(pcdata, "perm_hit", character.max_hit or 20))
|
|
73
|
-
db_char.perm_mana = int(getattr(pcdata, "perm_mana", character.max_mana or 100))
|
|
74
|
-
db_char.perm_move = int(getattr(pcdata, "perm_move", character.max_move or 100))
|
|
75
|
-
else:
|
|
76
|
-
db_char.perm_hit = int(character.max_hit or 20)
|
|
77
|
-
db_char.perm_mana = int(character.max_mana or 100)
|
|
78
|
-
db_char.perm_move = int(character.max_move or 100)
|
|
79
|
-
|
|
80
|
-
# Persist password hash if available on runtime character
|
|
81
|
-
if pcdata:
|
|
82
|
-
pwd = getattr(pcdata, "pwd", None) or ""
|
|
83
|
-
if pwd and not getattr(db_char, "password_hash", ""):
|
|
84
|
-
db_char.password_hash = pwd
|
|
85
|
-
|
|
86
|
-
db_char.default_weapon_vnum = int(character.default_weapon_vnum or 0)
|
|
87
|
-
db_char.creation_points = int(getattr(character, "creation_points", 0) or 0)
|
|
88
|
-
db_char.creation_groups = json.dumps(list(getattr(character, "creation_groups", ())))
|
|
89
|
-
db_char.newbie_help_seen = bool(getattr(character, "newbie_help_seen", False))
|
|
90
|
-
room = getattr(character, "room", None)
|
|
91
|
-
was_in_room = getattr(character, "was_in_room", None)
|
|
92
|
-
room_vnum = 0
|
|
93
|
-
if room is not None:
|
|
94
|
-
room_vnum = int(getattr(room, "vnum", 0) or 0)
|
|
95
|
-
if room_vnum == int(ROOM_VNUM_LIMBO) and was_in_room is not None:
|
|
96
|
-
room_vnum = int(getattr(was_in_room, "vnum", 0) or 0)
|
|
97
|
-
elif was_in_room is not None:
|
|
98
|
-
room_vnum = int(getattr(was_in_room, "vnum", 0) or 0)
|
|
99
|
-
if room_vnum <= 0:
|
|
100
|
-
room_vnum = int(ROOM_VNUM_TEMPLE)
|
|
101
|
-
db_char.room_vnum = room_vnum
|
|
102
|
-
save_objects_for_character(session, character, db_char)
|
|
103
|
-
session.commit()
|
|
91
|
+
save_character_to_db(session, character)
|
|
92
|
+
session.commit()
|
|
104
93
|
except Exception as e:
|
|
105
|
-
print(f"[ERROR]
|
|
94
|
+
print(f"[ERROR] save_character failed for {character.name}: {e}")
|
|
106
95
|
finally:
|
|
107
96
|
if session:
|
|
108
97
|
session.close()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def save_character_to_db(session: Session, character: Character) -> None:
|
|
101
|
+
"""Write all character state to the DB row — the canonical DB save path.
|
|
102
|
+
|
|
103
|
+
UPDATE-only: the character row must already exist (created via
|
|
104
|
+
account_service.create_character). If the row is not found, returns silently.
|
|
105
|
+
|
|
106
|
+
Replicates the nontrivial logic from the former mud.persistence.save_character:
|
|
107
|
+
- room_vnum LIMBO → TEMPLE fallback (mirroring ROM src/save.c:fwrite_char)
|
|
108
|
+
- played accumulation: base_played + (now - logon) (ROM src/save.c:fwrite_char)
|
|
109
|
+
- act flags reconciled with ansi_enabled (PlayerFlag.COLOUR bit)
|
|
110
|
+
- All 71 fields from INV008_REVERSAL_AUDIT §1
|
|
111
|
+
|
|
112
|
+
Does NOT commit the session — caller is responsible for session.commit()
|
|
113
|
+
so multiple saves can be batched.
|
|
114
|
+
|
|
115
|
+
ROM Reference: src/save.c fwrite_char / fwrite_pet
|
|
116
|
+
# mirroring mud/persistence.py:save_character
|
|
117
|
+
"""
|
|
118
|
+
from mud.models.constants import PlayerFlag
|
|
119
|
+
from mud.notes import DEFAULT_BOARD_NAME
|
|
120
|
+
|
|
121
|
+
db_char = session.query(DBCharacter).filter_by(name=character.name).first()
|
|
122
|
+
if db_char is None:
|
|
123
|
+
return # Character must exist — creation path handles inserts
|
|
124
|
+
|
|
125
|
+
pcdata = character.pcdata or __import__("mud.models.character", fromlist=["PCData"]).PCData()
|
|
126
|
+
|
|
127
|
+
# --- room_vnum: LIMBO fallback → TEMPLE (mirroring persistence.py:913-932) ---
|
|
128
|
+
room = getattr(character, "room", None)
|
|
129
|
+
current_vnum = getattr(room, "vnum", None)
|
|
130
|
+
if current_vnum == ROOM_VNUM_LIMBO:
|
|
131
|
+
was_in_room = getattr(character, "was_in_room", None)
|
|
132
|
+
fallback_vnum = getattr(was_in_room, "vnum", None)
|
|
133
|
+
if fallback_vnum is not None:
|
|
134
|
+
try:
|
|
135
|
+
room_vnum = int(fallback_vnum)
|
|
136
|
+
except (TypeError, ValueError):
|
|
137
|
+
room_vnum = ROOM_VNUM_TEMPLE
|
|
138
|
+
else:
|
|
139
|
+
room_vnum = ROOM_VNUM_TEMPLE
|
|
140
|
+
elif current_vnum is None:
|
|
141
|
+
room_vnum = ROOM_VNUM_TEMPLE
|
|
142
|
+
else:
|
|
143
|
+
try:
|
|
144
|
+
room_vnum = int(current_vnum)
|
|
145
|
+
except (TypeError, ValueError):
|
|
146
|
+
room_vnum = ROOM_VNUM_TEMPLE
|
|
147
|
+
|
|
148
|
+
# --- played accumulation (mirroring persistence.py:935-946) ---
|
|
149
|
+
now = int(time.time())
|
|
150
|
+
try:
|
|
151
|
+
logon_value = int(getattr(character, "logon", 0) or 0)
|
|
152
|
+
except (TypeError, ValueError):
|
|
153
|
+
logon_value = 0
|
|
154
|
+
try:
|
|
155
|
+
base_played = int(getattr(character, "played", 0) or 0)
|
|
156
|
+
except (TypeError, ValueError):
|
|
157
|
+
base_played = 0
|
|
158
|
+
session_played = max(0, now - logon_value) if logon_value else 0
|
|
159
|
+
total_played = max(0, base_played + session_played)
|
|
160
|
+
|
|
161
|
+
# --- act flags: reconcile ANSI colour bit (mirroring persistence.py:903-911) ---
|
|
162
|
+
ansi_enabled = bool(getattr(character, "ansi_enabled", True))
|
|
163
|
+
act_flags = int(getattr(character, "act", 0))
|
|
164
|
+
colour_bit = int(PlayerFlag.COLOUR)
|
|
165
|
+
if ansi_enabled:
|
|
166
|
+
act_flags |= colour_bit
|
|
167
|
+
else:
|
|
168
|
+
act_flags &= ~colour_bit
|
|
169
|
+
|
|
170
|
+
# --- scalar fields ---
|
|
171
|
+
db_char.level = character.level
|
|
172
|
+
db_char.hp = character.hit # column is named hp, field is hit
|
|
173
|
+
db_char.max_hit = character.max_hit
|
|
174
|
+
db_char.mana = character.mana
|
|
175
|
+
db_char.move = character.move
|
|
176
|
+
db_char.room_vnum = room_vnum
|
|
177
|
+
db_char.race = int(getattr(character, "race", 0))
|
|
178
|
+
db_char.ch_class = int(getattr(character, "ch_class", 0))
|
|
179
|
+
db_char.sex = int(getattr(character, "sex", 0))
|
|
180
|
+
db_char.true_sex = int(getattr(pcdata, "true_sex", 0))
|
|
181
|
+
db_char.alignment = int(getattr(character, "alignment", 0))
|
|
182
|
+
db_char.act = act_flags
|
|
183
|
+
db_char.practice = int(getattr(character, "practice", 0))
|
|
184
|
+
db_char.train = int(getattr(character, "train", 0))
|
|
185
|
+
db_char.perm_hit = int(getattr(pcdata, "perm_hit", 0))
|
|
186
|
+
db_char.perm_mana = int(getattr(pcdata, "perm_mana", 0))
|
|
187
|
+
db_char.perm_move = int(getattr(pcdata, "perm_move", 0))
|
|
188
|
+
db_char.gold = int(getattr(character, "gold", 0))
|
|
189
|
+
db_char.silver = int(getattr(character, "silver", 0))
|
|
190
|
+
db_char.exp = int(getattr(character, "exp", 0))
|
|
191
|
+
db_char.trust = int(getattr(character, "trust", 0))
|
|
192
|
+
db_char.invis_level = int(getattr(character, "invis_level", 0))
|
|
193
|
+
db_char.incog_level = int(getattr(character, "incog_level", 0))
|
|
194
|
+
db_char.saving_throw = int(getattr(character, "saving_throw", 0))
|
|
195
|
+
db_char.hitroll = int(getattr(character, "hitroll", 0))
|
|
196
|
+
db_char.damroll = int(getattr(character, "damroll", 0))
|
|
197
|
+
db_char.wimpy = int(getattr(character, "wimpy", 0))
|
|
198
|
+
db_char.position = int(getattr(character, "position", 8))
|
|
199
|
+
db_char.played = total_played
|
|
200
|
+
db_char.logon = logon_value
|
|
201
|
+
db_char.lines = int(getattr(character, "lines", 22))
|
|
202
|
+
db_char.prompt = getattr(character, "prompt", None)
|
|
203
|
+
prefix_value = getattr(character, "prefix", None)
|
|
204
|
+
db_char.prefix = str(prefix_value) if prefix_value is not None else None
|
|
205
|
+
db_char.affected_by = int(getattr(character, "affected_by", 0))
|
|
206
|
+
db_char.comm = int(getattr(character, "comm", 0))
|
|
207
|
+
db_char.wiznet = int(getattr(character, "wiznet", 0))
|
|
208
|
+
db_char.log_commands = bool(getattr(character, "log_commands", False))
|
|
209
|
+
db_char.newbie_help_seen = bool(getattr(character, "newbie_help_seen", False))
|
|
210
|
+
db_char.pfile_version = 1 # TABLES-001: always ROM-canonical on DB path
|
|
211
|
+
|
|
212
|
+
# --- pcdata scalar fields ---
|
|
213
|
+
db_char.title = getattr(pcdata, "title", None)
|
|
214
|
+
bamfin = getattr(pcdata, "bamfin", None)
|
|
215
|
+
db_char.bamfin = str(bamfin) if bamfin is not None else None
|
|
216
|
+
bamfout = getattr(pcdata, "bamfout", None)
|
|
217
|
+
db_char.bamfout = str(bamfout) if bamfout is not None else None
|
|
218
|
+
db_char.security = int(getattr(pcdata, "security", 0))
|
|
219
|
+
db_char.points = int(getattr(pcdata, "points", 0))
|
|
220
|
+
db_char.last_level = int(getattr(pcdata, "last_level", 0))
|
|
221
|
+
|
|
222
|
+
# --- password_hash sync ---
|
|
223
|
+
pwd = getattr(pcdata, "pwd", "") or ""
|
|
224
|
+
if pwd:
|
|
225
|
+
db_char.password_hash = pwd
|
|
226
|
+
|
|
227
|
+
# --- JSON collection fields ---
|
|
228
|
+
# skills: merge char.skills and pcdata.learned (mirroring persistence.py:898-901)
|
|
229
|
+
skills_snapshot = _serialize_skill_map(getattr(character, "skills", {}))
|
|
230
|
+
pcdata.learned = dict(skills_snapshot)
|
|
231
|
+
db_char.skills = skills_snapshot
|
|
232
|
+
|
|
233
|
+
# groups: from pcdata.group_known (mirroring persistence.py:899)
|
|
234
|
+
groups_snapshot = _serialize_groups(getattr(pcdata, "group_known", ()))
|
|
235
|
+
pcdata.group_known = tuple(groups_snapshot)
|
|
236
|
+
db_char.groups = groups_snapshot
|
|
237
|
+
|
|
238
|
+
# colours
|
|
239
|
+
colour_table = _serialize_colour_table(pcdata)
|
|
240
|
+
db_char.colours = colour_table
|
|
241
|
+
|
|
242
|
+
# conditions [drunk, full, thirst, hunger]
|
|
243
|
+
raw_conditions = list(getattr(pcdata, "condition", []))
|
|
244
|
+
conditions = [0, 48, 48, 48]
|
|
245
|
+
for idx, val in enumerate(raw_conditions[:4]):
|
|
246
|
+
try:
|
|
247
|
+
conditions[idx] = int(val)
|
|
248
|
+
except (TypeError, ValueError):
|
|
249
|
+
pass
|
|
250
|
+
db_char.conditions = conditions
|
|
251
|
+
|
|
252
|
+
# armor and mod_stat
|
|
253
|
+
db_char.armor = _normalize_int_list(getattr(character, "armor", []), 4)
|
|
254
|
+
db_char.mod_stat = _normalize_int_list(getattr(character, "mod_stat", []), 5)
|
|
255
|
+
|
|
256
|
+
# aliases
|
|
257
|
+
db_char.aliases = dict(getattr(character, "aliases", {}))
|
|
258
|
+
|
|
259
|
+
# board name
|
|
260
|
+
board_name = getattr(pcdata, "board_name", DEFAULT_BOARD_NAME) or DEFAULT_BOARD_NAME
|
|
261
|
+
db_char.board = board_name
|
|
262
|
+
|
|
263
|
+
# last_notes
|
|
264
|
+
db_char.last_notes = dict(getattr(pcdata, "last_notes", {}) or {})
|
|
265
|
+
|
|
266
|
+
# perm_stats JSON (already stored as string, keep existing column)
|
|
267
|
+
from mud.models.character import _encode_perm_stats
|
|
268
|
+
db_char.perm_stats = _encode_perm_stats(getattr(character, "perm_stat", []))
|
|
269
|
+
|
|
270
|
+
# creation_groups / creation_skills (keep in sync)
|
|
271
|
+
from mud.models.character import _encode_creation_groups, _encode_creation_skills
|
|
272
|
+
db_char.creation_groups = _encode_creation_groups(getattr(character, "creation_groups", ()))
|
|
273
|
+
db_char.creation_skills = _encode_creation_skills(getattr(character, "creation_skills", ()))
|
|
274
|
+
db_char.creation_points = int(getattr(character, "creation_points", 0) or 0)
|
|
275
|
+
|
|
276
|
+
# inventory as JSON blob (Option A from audit §3.1)
|
|
277
|
+
inventory_list = []
|
|
278
|
+
for obj in character.inventory:
|
|
279
|
+
try:
|
|
280
|
+
obj_save = _serialize_object(obj)
|
|
281
|
+
# Convert dataclass to dict for JSON storage
|
|
282
|
+
inventory_list.append(_dataclass_to_dict(obj_save))
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
285
|
+
db_char.inventory_state = inventory_list
|
|
286
|
+
|
|
287
|
+
# equipment as JSON blob (Option A from audit §3.1)
|
|
288
|
+
equipment_dict = {}
|
|
289
|
+
for slot, obj in character.equipment.items():
|
|
290
|
+
try:
|
|
291
|
+
obj_save = _serialize_object(obj, wear_slot=slot)
|
|
292
|
+
equipment_dict[slot] = _dataclass_to_dict(obj_save)
|
|
293
|
+
except Exception:
|
|
294
|
+
pass
|
|
295
|
+
db_char.equipment_state = equipment_dict
|
|
296
|
+
|
|
297
|
+
# pet as JSON blob (audit §3.1)
|
|
298
|
+
pet = getattr(character, "pet", None)
|
|
299
|
+
if pet is not None:
|
|
300
|
+
try:
|
|
301
|
+
pet_save = _serialize_pet(pet)
|
|
302
|
+
if pet_save is not None:
|
|
303
|
+
db_char.pet_state = _dataclass_to_dict(pet_save)
|
|
304
|
+
else:
|
|
305
|
+
db_char.pet_state = None
|
|
306
|
+
except Exception:
|
|
307
|
+
db_char.pet_state = None
|
|
308
|
+
else:
|
|
309
|
+
db_char.pet_state = None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _dataclass_to_dict(obj: object) -> dict:
|
|
313
|
+
"""Recursively convert a dataclass instance to a plain dict for JSON storage."""
|
|
314
|
+
import dataclasses
|
|
315
|
+
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
|
|
316
|
+
result = {}
|
|
317
|
+
for f in dataclasses.fields(obj): # type: ignore[arg-type]
|
|
318
|
+
val = getattr(obj, f.name)
|
|
319
|
+
if dataclasses.is_dataclass(val) and not isinstance(val, type):
|
|
320
|
+
result[f.name] = _dataclass_to_dict(val)
|
|
321
|
+
elif isinstance(val, list):
|
|
322
|
+
result[f.name] = [
|
|
323
|
+
_dataclass_to_dict(item) if (dataclasses.is_dataclass(item) and not isinstance(item, type)) else item
|
|
324
|
+
for item in val
|
|
325
|
+
]
|
|
326
|
+
elif isinstance(val, dict):
|
|
327
|
+
result[f.name] = {
|
|
328
|
+
k: (_dataclass_to_dict(v) if (dataclasses.is_dataclass(v) and not isinstance(v, type)) else v)
|
|
329
|
+
for k, v in val.items()
|
|
330
|
+
}
|
|
331
|
+
else:
|
|
332
|
+
result[f.name] = val
|
|
333
|
+
return result
|
|
334
|
+
return obj # type: ignore[return-value]
|
mud/account/account_service.py
CHANGED
|
@@ -1,46 +1,53 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from collections.abc import Iterable
|
|
3
|
-
from functools import lru_cache
|
|
4
3
|
from dataclasses import dataclass, field
|
|
5
4
|
from enum import Enum
|
|
5
|
+
from functools import lru_cache
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Final,
|
|
7
|
+
from typing import Final, NamedTuple
|
|
8
8
|
|
|
9
|
+
from mud.advancement import exp_per_level_for_creation
|
|
9
10
|
from mud.db.models import Character
|
|
10
11
|
from mud.db.session import SessionLocal
|
|
11
12
|
from mud.models.classes import (
|
|
12
13
|
ClassType,
|
|
14
|
+
)
|
|
15
|
+
from mud.models.classes import (
|
|
13
16
|
get_player_class as _get_player_class,
|
|
17
|
+
)
|
|
18
|
+
from mud.models.classes import (
|
|
14
19
|
list_player_classes as _list_player_classes,
|
|
15
20
|
)
|
|
16
21
|
from mud.models.constants import (
|
|
17
|
-
OBJ_VNUM_SCHOOL_DAGGER,
|
|
18
|
-
OBJ_VNUM_SCHOOL_MACE,
|
|
19
|
-
OBJ_VNUM_SCHOOL_SWORD,
|
|
20
|
-
PlayerFlag,
|
|
21
22
|
ROOM_VNUM_SCHOOL,
|
|
23
|
+
PlayerFlag,
|
|
22
24
|
Sex,
|
|
23
|
-
Stat,
|
|
24
25
|
)
|
|
25
26
|
from mud.models.races import (
|
|
26
27
|
PcRaceType,
|
|
27
28
|
RaceType,
|
|
29
|
+
)
|
|
30
|
+
from mud.models.races import (
|
|
28
31
|
get_pc_race as _get_pc_race,
|
|
32
|
+
)
|
|
33
|
+
from mud.models.races import (
|
|
29
34
|
get_race as _get_race,
|
|
35
|
+
)
|
|
36
|
+
from mud.models.races import (
|
|
30
37
|
list_playable_races as _list_playable_races,
|
|
31
38
|
)
|
|
39
|
+
from mud.models.titles import default_title_storage
|
|
40
|
+
from mud.models.weapon_table import weapon_table_entry_by_name
|
|
32
41
|
from mud.security import bans
|
|
33
42
|
from mud.security.bans import BanFlag
|
|
34
43
|
from mud.security.hash_utils import hash_password, verify_password
|
|
35
44
|
from mud.skills.groups import get_group, iter_group_names, list_groups
|
|
36
45
|
from mud.skills.metadata import ROM_SKILL_METADATA, ROM_SKILL_NAMES_BY_INDEX
|
|
46
|
+
from mud.utils import rng_mm
|
|
37
47
|
from mud.world.world_state import (
|
|
38
48
|
is_newlock_enabled,
|
|
39
49
|
is_wizlock_enabled,
|
|
40
50
|
)
|
|
41
|
-
from mud.utils import rng_mm
|
|
42
|
-
|
|
43
|
-
from mud.advancement import exp_per_level_for_creation
|
|
44
51
|
|
|
45
52
|
|
|
46
53
|
class LoginFailureReason(Enum):
|
|
@@ -94,13 +101,6 @@ _WEAPON_CHOICES: Final[dict[str, tuple[str, ...]]] = {
|
|
|
94
101
|
"warrior": ("sword", "mace"),
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
_WEAPON_VNUMS: Final[dict[str, int]] = {
|
|
98
|
-
"dagger": OBJ_VNUM_SCHOOL_DAGGER,
|
|
99
|
-
"mace": OBJ_VNUM_SCHOOL_MACE,
|
|
100
|
-
"sword": OBJ_VNUM_SCHOOL_SWORD,
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
104
|
@lru_cache(maxsize=1)
|
|
105
105
|
def _load_skill_data() -> dict[str, dict[str, object]]:
|
|
106
106
|
"""Return ROM skill metadata (type/ratings) keyed by lower-case name."""
|
|
@@ -563,7 +563,8 @@ def get_weapon_choices(class_type: ClassType) -> tuple[str, ...]:
|
|
|
563
563
|
def lookup_weapon_choice(name: str) -> int | None:
|
|
564
564
|
"""Map a weapon name from creation prompts to its object vnum."""
|
|
565
565
|
|
|
566
|
-
|
|
566
|
+
entry = weapon_table_entry_by_name(name)
|
|
567
|
+
return entry.school_vnum if entry is not None else None
|
|
567
568
|
|
|
568
569
|
|
|
569
570
|
def sanitize_account_name(username: str) -> str:
|
|
@@ -1062,8 +1063,12 @@ def create_character(
|
|
|
1062
1063
|
"perm_move": 100,
|
|
1063
1064
|
"act": int(PlayerFlag.NOSUMMON),
|
|
1064
1065
|
"default_weapon_vnum": weapon_vnum,
|
|
1066
|
+
"title": default_title_storage(_class_index_for(selected_class), 1, sex_value),
|
|
1065
1067
|
"newbie_help_seen": False,
|
|
1066
1068
|
"creation_points": int(creation_points_value),
|
|
1069
|
+
# mirroring ROM src/nanny.c: the creation point total is persistent
|
|
1070
|
+
# character state, so keep DB-canonical `points` in sync on initial save.
|
|
1071
|
+
"points": int(creation_points_value),
|
|
1067
1072
|
"creation_groups": json.dumps(list(groups_tuple)),
|
|
1068
1073
|
"creation_skills": json.dumps(list(skills_tuple)),
|
|
1069
1074
|
}
|
mud/advancement.py
CHANGED
|
@@ -2,12 +2,14 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
4
|
|
|
5
|
+
from mud.logging import log_game_event
|
|
5
6
|
from mud.math.c_compat import c_div
|
|
6
7
|
from mud.math.stat_apps import con_hitp_bonus, wis_practice_bonus
|
|
7
8
|
from mud.models.character import Character
|
|
8
9
|
from mud.models.classes import CLASS_TABLE, ClassType
|
|
9
10
|
from mud.models.constants import LEVEL_HERO
|
|
10
11
|
from mud.models.races import PcRaceType, list_playable_races
|
|
12
|
+
from mud.models.titles import default_title_text
|
|
11
13
|
from mud.utils import rng_mm
|
|
12
14
|
from mud.wiznet import WiznetFlag, wiznet
|
|
13
15
|
|
|
@@ -138,6 +140,9 @@ def advance_level(char: Character) -> None:
|
|
|
138
140
|
pcdata.perm_hit = int(getattr(pcdata, "perm_hit", 0) or 0) + hp
|
|
139
141
|
pcdata.perm_mana = int(getattr(pcdata, "perm_mana", 0) or 0) + mana
|
|
140
142
|
pcdata.perm_move = int(getattr(pcdata, "perm_move", 0) or 0) + move
|
|
143
|
+
from mud.commands.character import set_title
|
|
144
|
+
|
|
145
|
+
set_title(char, default_title_text(char.ch_class, char.level, getattr(char, "sex", 0)))
|
|
141
146
|
|
|
142
147
|
if hasattr(char, "send_to_char") and not getattr(char, "is_npc", False):
|
|
143
148
|
hit_suffix = "" if hp == 1 else "s"
|
|
@@ -212,10 +217,10 @@ def gain_exp(char: Character, amount: int) -> None:
|
|
|
212
217
|
|
|
213
218
|
# Level up while total exp meets threshold for next level.
|
|
214
219
|
while char.level < LEVEL_HERO and char.exp >= exp_per_level(char) * (char.level + 1):
|
|
215
|
-
char.level += 1
|
|
216
|
-
advance_level(char)
|
|
217
220
|
if hasattr(char, "send_to_char"):
|
|
218
221
|
char.send_to_char("{GYou raise a level!! {x")
|
|
222
|
+
char.level += 1
|
|
223
|
+
log_game_event(f"{getattr(char, 'name', 'Someone')} gained level {char.level}")
|
|
219
224
|
wiznet(
|
|
220
225
|
f"$N has attained level {char.level}!",
|
|
221
226
|
char,
|
|
@@ -224,6 +229,7 @@ def gain_exp(char: Character, amount: int) -> None:
|
|
|
224
229
|
None,
|
|
225
230
|
0,
|
|
226
231
|
)
|
|
232
|
+
advance_level(char)
|
|
227
233
|
# Lazy import to avoid circular dependency
|
|
228
234
|
from mud.account.account_manager import save_character
|
|
229
235
|
save_character(char)
|
mud/affects/engine.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from mud.models.character import Character
|
|
4
|
+
from mud.utils import rng_mm
|
|
4
5
|
|
|
5
6
|
ROM_NEWLINE = "\n\r"
|
|
6
7
|
|
|
@@ -15,6 +16,59 @@ def tick_spell_effects(character: Character) -> list[str]:
|
|
|
15
16
|
if not isinstance(effects, dict):
|
|
16
17
|
return messages
|
|
17
18
|
|
|
19
|
+
affected = getattr(character, "affected", None)
|
|
20
|
+
if isinstance(affected, list) and affected:
|
|
21
|
+
touched_names: set[str] = set()
|
|
22
|
+
ordered_affects = list(affected)
|
|
23
|
+
|
|
24
|
+
for index, affect in enumerate(ordered_affects):
|
|
25
|
+
duration = int(getattr(affect, "duration", 0) or 0)
|
|
26
|
+
if duration > 0:
|
|
27
|
+
affect.duration = duration - 1
|
|
28
|
+
level = int(getattr(affect, "level", 0) or 0)
|
|
29
|
+
if level > 0 and rng_mm.number_range(0, 4) == 0:
|
|
30
|
+
affect.level = level - 1 # mirroring ROM src/update.c:765-768
|
|
31
|
+
spell_name = getattr(affect, "type", None)
|
|
32
|
+
if isinstance(spell_name, str) and spell_name in effects:
|
|
33
|
+
touched_names.add(spell_name)
|
|
34
|
+
continue
|
|
35
|
+
if duration < 0:
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
spell_name = getattr(affect, "type", None)
|
|
39
|
+
next_affect = ordered_affects[index + 1] if index + 1 < len(ordered_affects) else None
|
|
40
|
+
should_emit = (
|
|
41
|
+
next_affect is None
|
|
42
|
+
or getattr(next_affect, "type", None) != spell_name
|
|
43
|
+
or int(getattr(next_affect, "duration", 0) or 0) > 0
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if affect in affected:
|
|
47
|
+
affected.remove(affect)
|
|
48
|
+
|
|
49
|
+
if isinstance(spell_name, str) and spell_name in effects:
|
|
50
|
+
touched_names.add(spell_name)
|
|
51
|
+
wear_off = getattr(effects[spell_name], "wear_off_message", None)
|
|
52
|
+
if should_emit and wear_off:
|
|
53
|
+
messages.append(f"{wear_off}{ROM_NEWLINE}")
|
|
54
|
+
|
|
55
|
+
for spell_name in touched_names:
|
|
56
|
+
remaining = [
|
|
57
|
+
affect
|
|
58
|
+
for affect in getattr(character, "affected", [])
|
|
59
|
+
if getattr(affect, "type", None) == spell_name
|
|
60
|
+
]
|
|
61
|
+
if remaining:
|
|
62
|
+
primary = remaining[0]
|
|
63
|
+
effect = effects.get(spell_name)
|
|
64
|
+
if effect is not None:
|
|
65
|
+
effect.duration = int(getattr(primary, "duration", 0) or 0)
|
|
66
|
+
effect.level = int(getattr(primary, "level", 0) or 0)
|
|
67
|
+
continue
|
|
68
|
+
character.remove_spell_effect(spell_name)
|
|
69
|
+
|
|
70
|
+
return messages
|
|
71
|
+
|
|
18
72
|
for name, effect in list(effects.items()):
|
|
19
73
|
duration = int(getattr(effect, "duration", 0) or 0)
|
|
20
74
|
if duration > 0:
|