rom24-quickmud-python 2.5.4__py3-none-any.whl → 2.8.21__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 +1 -0
- mud/account/__init__.py +16 -2
- mud/account/account_manager.py +300 -107
- mud/account/account_service.py +296 -169
- mud/admin_logging/__init__.py +1 -0
- mud/advancement.py +40 -8
- mud/affects/engine.py +50 -0
- mud/agent/__init__.py +1 -0
- mud/ai/__init__.py +2 -55
- mud/combat/assist.py +10 -4
- mud/combat/engine.py +145 -105
- mud/commands/admin_commands.py +85 -90
- mud/commands/advancement.py +188 -17
- mud/commands/affects.py +34 -1
- mud/commands/alias_cmds.py +102 -26
- mud/commands/auto_settings.py +143 -18
- mud/commands/build.py +2051 -188
- mud/commands/character.py +41 -31
- mud/commands/combat.py +8 -4
- mud/commands/communication.py +66 -24
- mud/commands/consumption.py +261 -165
- mud/commands/dispatcher.py +456 -198
- mud/commands/doors.py +227 -130
- mud/commands/equipment.py +281 -165
- mud/commands/give.py +217 -168
- mud/commands/group_commands.py +255 -150
- mud/commands/healer.py +222 -53
- mud/commands/imm_admin.py +170 -122
- mud/commands/imm_commands.py +94 -68
- mud/commands/imm_display.py +198 -151
- mud/commands/imm_emote.py +167 -102
- mud/commands/imm_load.py +133 -113
- mud/commands/imm_olc.py +706 -170
- mud/commands/imm_punish.py +154 -151
- mud/commands/imm_search.py +850 -306
- mud/commands/imm_server.py +122 -106
- mud/commands/imm_set.py +490 -397
- mud/commands/info.py +4 -2
- mud/commands/info_extended.py +1 -76
- mud/commands/inspection.py +13 -5
- mud/commands/inventory.py +563 -36
- mud/commands/liquids.py +138 -63
- mud/commands/magic_items.py +296 -384
- mud/commands/misc_player.py +3 -3
- mud/commands/movement.py +34 -23
- mud/commands/notes.py +81 -29
- mud/commands/obj_manipulation.py +225 -67
- mud/commands/player_info.py +105 -65
- mud/commands/position.py +421 -40
- mud/commands/remaining_rom.py +271 -240
- mud/commands/session.py +89 -88
- mud/commands/shop.py +131 -32
- mud/commands/socials.py +57 -6
- mud/commands/thief_skills.py +324 -234
- mud/commands/typo_guards.py +10 -34
- mud/db/__init__.py +1 -0
- mud/db/migrations.py +78 -0
- mud/db/models.py +58 -21
- mud/db/seed.py +11 -12
- mud/db/serializers.py +622 -0
- mud/devtools/__init__.py +1 -0
- mud/game_loop.py +242 -41
- mud/groups/xp.py +30 -30
- mud/handler.py +280 -22
- mud/loaders/json_loader.py +311 -29
- mud/loaders/mob_loader.py +75 -7
- mud/loaders/obj_loader.py +6 -5
- mud/magic/effects.py +20 -13
- mud/math/stat_apps.py +294 -0
- mud/mob_cmds.py +298 -52
- mud/mobprog.py +121 -40
- mud/models/board.py +26 -2
- mud/models/character.py +189 -18
- mud/models/clans.py +3 -1
- mud/models/constants.py +123 -69
- mud/models/mob.py +25 -0
- mud/models/object.py +30 -2
- mud/models/races.py +17 -0
- mud/models/room.py +14 -1
- mud/models/social.py +21 -0
- mud/models/titles.py +292 -0
- mud/models/weapon_table.py +86 -0
- mud/music/__init__.py +173 -10
- mud/net/ansi.py +54 -8
- mud/net/connection.py +508 -128
- mud/net/session.py +8 -0
- mud/network/__init__.py +1 -0
- mud/network/websocket_server.py +38 -68
- mud/network/websocket_stream.py +165 -0
- mud/notes.py +253 -2
- mud/olc/editor_state.py +79 -0
- mud/olc/save.py +246 -5
- mud/rom_api.py +1 -1
- mud/scripts/convert_are_to_json.py +24 -7
- mud/scripts/load_test_data.py +6 -13
- mud/security/__init__.py +1 -0
- mud/security/bans.py +3 -1
- mud/skills/handlers.py +43 -3
- mud/skills/registry.py +19 -7
- mud/spawning/mob_spawner.py +7 -0
- mud/spawning/reset_handler.py +11 -12
- mud/spawning/templates.py +23 -2
- mud/spec_funs.py +142 -64
- mud/utils/act.py +9 -5
- mud/utils/bit.py +122 -0
- mud/utils/fix_sex.py +24 -0
- mud/utils/olc_tables.py +127 -0
- mud/utils/poses.py +187 -0
- mud/utils/prefix_lookup.py +217 -0
- mud/utils/prompt.py +259 -0
- mud/utils/string_editor.py +581 -0
- mud/wiznet.py +119 -46
- mud/world/char_find.py +6 -2
- mud/world/look.py +20 -6
- mud/world/movement.py +138 -50
- mud/world/obj_find.py +3 -3
- mud/world/time_persistence.py +61 -0
- mud/world/world_state.py +17 -0
- {rom24_quickmud_python-2.5.4.dist-info → rom24_quickmud_python-2.8.21.dist-info}/METADATA +87 -21
- rom24_quickmud_python-2.8.21.dist-info/RECORD +224 -0
- {rom24_quickmud_python-2.5.4.dist-info → rom24_quickmud_python-2.8.21.dist-info}/WHEEL +1 -1
- mud/persistence.py +0 -1147
- rom24_quickmud_python-2.5.4.dist-info/RECORD +0 -211
- {rom24_quickmud_python-2.5.4.dist-info → rom24_quickmud_python-2.8.21.dist-info}/entry_points.txt +0 -0
- {rom24_quickmud_python-2.5.4.dist-info → rom24_quickmud_python-2.8.21.dist-info}/licenses/LICENSE +0 -0
- {rom24_quickmud_python-2.5.4.dist-info → rom24_quickmud_python-2.8.21.dist-info}/top_level.txt +0 -0
mud/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Package marker for mud."""
|
mud/account/__init__.py
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
"""Account management utilities.
|
|
1
|
+
"""Account management utilities.
|
|
2
|
+
|
|
3
|
+
The Python-only PlayerAccount layer has been removed. Characters now own
|
|
4
|
+
their passwords directly, mirroring ROM src/merc.h PCData.pwd.
|
|
5
|
+
"""
|
|
2
6
|
|
|
3
7
|
from .account_manager import load_character, save_character
|
|
4
8
|
from .account_service import (
|
|
5
9
|
CreationSelection,
|
|
6
10
|
account_exists,
|
|
11
|
+
character_exists,
|
|
7
12
|
create_account,
|
|
8
13
|
create_character,
|
|
14
|
+
create_character_record,
|
|
9
15
|
clear_active_accounts,
|
|
10
16
|
get_creation_classes,
|
|
11
17
|
get_creation_races,
|
|
@@ -14,6 +20,8 @@ from .account_service import (
|
|
|
14
20
|
get_race_archetype,
|
|
15
21
|
is_account_active,
|
|
16
22
|
is_valid_account_name,
|
|
23
|
+
is_valid_character_name,
|
|
24
|
+
login_character,
|
|
17
25
|
lookup_creation_class,
|
|
18
26
|
lookup_creation_race,
|
|
19
27
|
lookup_hometown,
|
|
@@ -27,17 +35,21 @@ from .account_service import (
|
|
|
27
35
|
finalize_creation_stats,
|
|
28
36
|
sanitize_account_name,
|
|
29
37
|
release_account,
|
|
38
|
+
release_character,
|
|
30
39
|
)
|
|
31
40
|
|
|
32
41
|
__all__ = [
|
|
33
42
|
"load_character",
|
|
34
43
|
"save_character",
|
|
35
44
|
"account_exists",
|
|
45
|
+
"character_exists",
|
|
36
46
|
"create_account",
|
|
47
|
+
"create_character",
|
|
48
|
+
"create_character_record",
|
|
37
49
|
"login",
|
|
50
|
+
"login_character",
|
|
38
51
|
"login_with_host",
|
|
39
52
|
"list_characters",
|
|
40
|
-
"create_character",
|
|
41
53
|
"CreationSelection",
|
|
42
54
|
"get_creation_races",
|
|
43
55
|
"lookup_creation_race",
|
|
@@ -51,10 +63,12 @@ __all__ = [
|
|
|
51
63
|
"roll_creation_stats",
|
|
52
64
|
"finalize_creation_stats",
|
|
53
65
|
"is_valid_account_name",
|
|
66
|
+
"is_valid_character_name",
|
|
54
67
|
"sanitize_account_name",
|
|
55
68
|
"clear_active_accounts",
|
|
56
69
|
"is_account_active",
|
|
57
70
|
"release_account",
|
|
71
|
+
"release_character",
|
|
58
72
|
"LoginFailureReason",
|
|
59
73
|
"LoginResult",
|
|
60
74
|
]
|
mud/account/account_manager.py
CHANGED
|
@@ -1,38 +1,69 @@
|
|
|
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
|
-
from mud.db.models import PlayerAccount
|
|
7
27
|
from mud.db.session import SessionLocal
|
|
8
|
-
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from sqlalchemy.orm import Session
|
|
31
|
+
from mud.models.character import Character, character_registry, from_orm
|
|
9
32
|
from mud.models.constants import ROOM_VNUM_LIMBO, ROOM_VNUM_TEMPLE
|
|
10
|
-
from mud.
|
|
11
|
-
|
|
12
|
-
|
|
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,
|
|
13
40
|
)
|
|
14
41
|
|
|
15
42
|
|
|
16
|
-
def load_character(
|
|
43
|
+
def load_character(char_name: str, _ignored: str | None = None) -> Character | None:
|
|
44
|
+
"""Load a character by name from the DB row (DB-canonical path).
|
|
45
|
+
|
|
46
|
+
The second argument is accepted but ignored for backward compatibility;
|
|
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).
|
|
51
|
+
"""
|
|
17
52
|
session = None
|
|
18
53
|
try:
|
|
19
54
|
session = SessionLocal()
|
|
20
|
-
db_char = (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
char = from_orm(db_char) if db_char else None
|
|
30
|
-
if char and db_char:
|
|
31
|
-
_ = db_char.player # load relationship
|
|
32
|
-
char.inventory, char.equipment = load_objects_for_character(db_char)
|
|
55
|
+
db_char = session.query(DBCharacter).filter(DBCharacter.name == char_name).first()
|
|
56
|
+
if db_char is None:
|
|
57
|
+
return None
|
|
58
|
+
char = from_orm(db_char)
|
|
59
|
+
if char is not None:
|
|
60
|
+
# Use identity check to avoid triggering dataclass __eq__ on deep
|
|
61
|
+
# object graphs (inventory/equipment items cause recursion with list `in`)
|
|
62
|
+
if not any(c is char for c in character_registry):
|
|
63
|
+
character_registry.append(char) # INV-003
|
|
33
64
|
return char
|
|
34
65
|
except Exception as e:
|
|
35
|
-
print(f"[ERROR] Failed to load character {char_name}: {e}")
|
|
66
|
+
print(f"[ERROR] Failed to load character {char_name} from DB: {e}")
|
|
36
67
|
return None
|
|
37
68
|
finally:
|
|
38
69
|
if session:
|
|
@@ -40,96 +71,258 @@ def load_character(username: str, char_name: str) -> Character | None:
|
|
|
40
71
|
|
|
41
72
|
|
|
42
73
|
def save_character(character: Character) -> None:
|
|
74
|
+
"""Persist ``character`` to the DB row (DB-canonical path).
|
|
75
|
+
|
|
76
|
+
UPDATE-only: the character row must already exist (created via
|
|
77
|
+
account_service.create_character). If the row is not found, returns silently.
|
|
78
|
+
|
|
79
|
+
ROM Reference: src/save.c fwrite_char — all fields persisted.
|
|
80
|
+
# mirroring mud/persistence.py:save_character (now removed for JSON path)
|
|
81
|
+
"""
|
|
43
82
|
session = None
|
|
44
83
|
try:
|
|
45
84
|
session = SessionLocal()
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# Character doesn't exist in database - create it
|
|
49
|
-
# This handles cases where character was created via JSON or other means
|
|
50
|
-
print(f"[WARN] Character '{character.name}' not found in database, creating new record")
|
|
51
|
-
|
|
52
|
-
# CRITICAL: Try to find and link the player account
|
|
53
|
-
player_id = None
|
|
54
|
-
pcdata = getattr(character, "pcdata", None)
|
|
55
|
-
if pcdata:
|
|
56
|
-
account_name = getattr(pcdata, "account_name", None)
|
|
57
|
-
if account_name:
|
|
58
|
-
player_account = session.query(PlayerAccount).filter_by(username=account_name).first()
|
|
59
|
-
if player_account:
|
|
60
|
-
player_id = player_account.id
|
|
61
|
-
print(f"[INFO] Linked character '{character.name}' to account '{account_name}' (id={player_id})")
|
|
62
|
-
else:
|
|
63
|
-
print(f"[WARN] Could not find player account '{account_name}' for character '{character.name}'")
|
|
64
|
-
|
|
65
|
-
db_char = DBCharacter(name=character.name, player_id=player_id)
|
|
66
|
-
session.add(db_char)
|
|
67
|
-
|
|
68
|
-
# Update all fields
|
|
69
|
-
if db_char:
|
|
70
|
-
# Ensure player_id is set if we have account information
|
|
71
|
-
if not db_char.player_id:
|
|
72
|
-
pcdata = getattr(character, "pcdata", None)
|
|
73
|
-
if pcdata:
|
|
74
|
-
account_name = getattr(pcdata, "account_name", None)
|
|
75
|
-
if account_name:
|
|
76
|
-
player_account = session.query(PlayerAccount).filter_by(username=account_name).first()
|
|
77
|
-
if player_account:
|
|
78
|
-
db_char.player_id = player_account.id
|
|
79
|
-
print(f"[INFO] Fixed missing player_id for character '{character.name}' -> account '{account_name}'")
|
|
80
|
-
|
|
81
|
-
db_char.level = character.level
|
|
82
|
-
db_char.hp = character.hit
|
|
83
|
-
db_char.race = int(character.race or 0)
|
|
84
|
-
db_char.ch_class = int(character.ch_class or 0)
|
|
85
|
-
pcdata = getattr(character, "pcdata", None)
|
|
86
|
-
true_sex_value = int(getattr(pcdata, "true_sex", getattr(character, "sex", 0)) or 0)
|
|
87
|
-
db_char.true_sex = true_sex_value
|
|
88
|
-
db_char.sex = int(character.sex or true_sex_value or 0)
|
|
89
|
-
db_char.alignment = int(character.alignment or 0)
|
|
90
|
-
db_char.act = int(getattr(character, "act", 0) or 0)
|
|
91
|
-
db_char.hometown_vnum = int(character.hometown_vnum or 0)
|
|
92
|
-
db_char.perm_stats = json.dumps([int(val) for val in character.perm_stat])
|
|
93
|
-
db_char.size = int(character.size or 0)
|
|
94
|
-
db_char.form = int(character.form or 0)
|
|
95
|
-
db_char.parts = int(character.parts or 0)
|
|
96
|
-
db_char.imm_flags = int(character.imm_flags or 0)
|
|
97
|
-
db_char.res_flags = int(character.res_flags or 0)
|
|
98
|
-
db_char.vuln_flags = int(character.vuln_flags or 0)
|
|
99
|
-
db_char.practice = int(character.practice or 0)
|
|
100
|
-
db_char.train = int(character.train or 0)
|
|
101
|
-
|
|
102
|
-
# Save perm stats from pcdata (ROM src/handler.c stores perm_hit/perm_mana/perm_move)
|
|
103
|
-
if pcdata:
|
|
104
|
-
db_char.perm_hit = int(getattr(pcdata, "perm_hit", character.max_hit or 20))
|
|
105
|
-
db_char.perm_mana = int(getattr(pcdata, "perm_mana", character.max_mana or 100))
|
|
106
|
-
db_char.perm_move = int(getattr(pcdata, "perm_move", character.max_move or 100))
|
|
107
|
-
else:
|
|
108
|
-
# Fallback if no pcdata
|
|
109
|
-
db_char.perm_hit = int(character.max_hit or 20)
|
|
110
|
-
db_char.perm_mana = int(character.max_mana or 100)
|
|
111
|
-
db_char.perm_move = int(character.max_move or 100)
|
|
112
|
-
|
|
113
|
-
db_char.default_weapon_vnum = int(character.default_weapon_vnum or 0)
|
|
114
|
-
db_char.creation_points = int(getattr(character, "creation_points", 0) or 0)
|
|
115
|
-
db_char.creation_groups = json.dumps(list(getattr(character, "creation_groups", ())))
|
|
116
|
-
db_char.newbie_help_seen = bool(getattr(character, "newbie_help_seen", False))
|
|
117
|
-
room = getattr(character, "room", None)
|
|
118
|
-
was_in_room = getattr(character, "was_in_room", None)
|
|
119
|
-
room_vnum = 0
|
|
120
|
-
if room is not None:
|
|
121
|
-
room_vnum = int(getattr(room, "vnum", 0) or 0)
|
|
122
|
-
if room_vnum == int(ROOM_VNUM_LIMBO) and was_in_room is not None:
|
|
123
|
-
room_vnum = int(getattr(was_in_room, "vnum", 0) or 0)
|
|
124
|
-
elif was_in_room is not None:
|
|
125
|
-
room_vnum = int(getattr(was_in_room, "vnum", 0) or 0)
|
|
126
|
-
if room_vnum <= 0:
|
|
127
|
-
room_vnum = int(ROOM_VNUM_TEMPLE)
|
|
128
|
-
db_char.room_vnum = room_vnum
|
|
129
|
-
save_objects_for_character(session, character, db_char)
|
|
130
|
-
session.commit()
|
|
85
|
+
save_character_to_db(session, character)
|
|
86
|
+
session.commit()
|
|
131
87
|
except Exception as e:
|
|
132
|
-
print(f"[ERROR]
|
|
88
|
+
print(f"[ERROR] save_character failed for {character.name}: {e}")
|
|
133
89
|
finally:
|
|
134
90
|
if session:
|
|
135
91
|
session.close()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def save_character_to_db(session: Session, character: Character) -> None:
|
|
95
|
+
"""Write all character state to the DB row — the canonical DB save path.
|
|
96
|
+
|
|
97
|
+
UPDATE-only: the character row must already exist (created via
|
|
98
|
+
account_service.create_character). If the row is not found, returns silently.
|
|
99
|
+
|
|
100
|
+
Replicates the nontrivial logic from the former mud.persistence.save_character:
|
|
101
|
+
- room_vnum LIMBO → TEMPLE fallback (mirroring ROM src/save.c:fwrite_char)
|
|
102
|
+
- played accumulation: base_played + (now - logon) (ROM src/save.c:fwrite_char)
|
|
103
|
+
- act flags reconciled with ansi_enabled (PlayerFlag.COLOUR bit)
|
|
104
|
+
- All 71 fields from INV008_REVERSAL_AUDIT §1
|
|
105
|
+
|
|
106
|
+
Does NOT commit the session — caller is responsible for session.commit()
|
|
107
|
+
so multiple saves can be batched.
|
|
108
|
+
|
|
109
|
+
ROM Reference: src/save.c fwrite_char / fwrite_pet
|
|
110
|
+
# mirroring mud/persistence.py:save_character
|
|
111
|
+
"""
|
|
112
|
+
from mud.models.constants import PlayerFlag
|
|
113
|
+
from mud.notes import DEFAULT_BOARD_NAME
|
|
114
|
+
|
|
115
|
+
db_char = session.query(DBCharacter).filter_by(name=character.name).first()
|
|
116
|
+
if db_char is None:
|
|
117
|
+
return # Character must exist — creation path handles inserts
|
|
118
|
+
|
|
119
|
+
pcdata = character.pcdata or __import__("mud.models.character", fromlist=["PCData"]).PCData()
|
|
120
|
+
|
|
121
|
+
# --- room_vnum: LIMBO fallback → TEMPLE (mirroring persistence.py:913-932) ---
|
|
122
|
+
room = getattr(character, "room", None)
|
|
123
|
+
current_vnum = getattr(room, "vnum", None)
|
|
124
|
+
if current_vnum == ROOM_VNUM_LIMBO:
|
|
125
|
+
was_in_room = getattr(character, "was_in_room", None)
|
|
126
|
+
fallback_vnum = getattr(was_in_room, "vnum", None)
|
|
127
|
+
if fallback_vnum is not None:
|
|
128
|
+
try:
|
|
129
|
+
room_vnum = int(fallback_vnum)
|
|
130
|
+
except (TypeError, ValueError):
|
|
131
|
+
room_vnum = ROOM_VNUM_TEMPLE
|
|
132
|
+
else:
|
|
133
|
+
room_vnum = ROOM_VNUM_TEMPLE
|
|
134
|
+
elif current_vnum is None:
|
|
135
|
+
room_vnum = ROOM_VNUM_TEMPLE
|
|
136
|
+
else:
|
|
137
|
+
try:
|
|
138
|
+
room_vnum = int(current_vnum)
|
|
139
|
+
except (TypeError, ValueError):
|
|
140
|
+
room_vnum = ROOM_VNUM_TEMPLE
|
|
141
|
+
|
|
142
|
+
# --- played accumulation (mirroring persistence.py:935-946) ---
|
|
143
|
+
now = int(time.time())
|
|
144
|
+
try:
|
|
145
|
+
logon_value = int(getattr(character, "logon", 0) or 0)
|
|
146
|
+
except (TypeError, ValueError):
|
|
147
|
+
logon_value = 0
|
|
148
|
+
try:
|
|
149
|
+
base_played = int(getattr(character, "played", 0) or 0)
|
|
150
|
+
except (TypeError, ValueError):
|
|
151
|
+
base_played = 0
|
|
152
|
+
session_played = max(0, now - logon_value) if logon_value else 0
|
|
153
|
+
total_played = max(0, base_played + session_played)
|
|
154
|
+
|
|
155
|
+
# --- act flags: reconcile ANSI colour bit (mirroring persistence.py:903-911) ---
|
|
156
|
+
ansi_enabled = bool(getattr(character, "ansi_enabled", True))
|
|
157
|
+
act_flags = int(getattr(character, "act", 0))
|
|
158
|
+
colour_bit = int(PlayerFlag.COLOUR)
|
|
159
|
+
if ansi_enabled:
|
|
160
|
+
act_flags |= colour_bit
|
|
161
|
+
else:
|
|
162
|
+
act_flags &= ~colour_bit
|
|
163
|
+
|
|
164
|
+
# --- scalar fields ---
|
|
165
|
+
db_char.level = character.level
|
|
166
|
+
db_char.hp = character.hit # column is named hp, field is hit
|
|
167
|
+
db_char.max_hit = character.max_hit
|
|
168
|
+
db_char.mana = character.mana
|
|
169
|
+
db_char.move = character.move
|
|
170
|
+
db_char.room_vnum = room_vnum
|
|
171
|
+
db_char.race = int(getattr(character, "race", 0))
|
|
172
|
+
db_char.ch_class = int(getattr(character, "ch_class", 0))
|
|
173
|
+
db_char.sex = int(getattr(character, "sex", 0))
|
|
174
|
+
db_char.true_sex = int(getattr(pcdata, "true_sex", 0))
|
|
175
|
+
db_char.alignment = int(getattr(character, "alignment", 0))
|
|
176
|
+
db_char.act = act_flags
|
|
177
|
+
db_char.practice = int(getattr(character, "practice", 0))
|
|
178
|
+
db_char.train = int(getattr(character, "train", 0))
|
|
179
|
+
db_char.perm_hit = int(getattr(pcdata, "perm_hit", 0))
|
|
180
|
+
db_char.perm_mana = int(getattr(pcdata, "perm_mana", 0))
|
|
181
|
+
db_char.perm_move = int(getattr(pcdata, "perm_move", 0))
|
|
182
|
+
db_char.gold = int(getattr(character, "gold", 0))
|
|
183
|
+
db_char.silver = int(getattr(character, "silver", 0))
|
|
184
|
+
db_char.exp = int(getattr(character, "exp", 0))
|
|
185
|
+
db_char.trust = int(getattr(character, "trust", 0))
|
|
186
|
+
db_char.invis_level = int(getattr(character, "invis_level", 0))
|
|
187
|
+
db_char.incog_level = int(getattr(character, "incog_level", 0))
|
|
188
|
+
db_char.saving_throw = int(getattr(character, "saving_throw", 0))
|
|
189
|
+
db_char.hitroll = int(getattr(character, "hitroll", 0))
|
|
190
|
+
db_char.damroll = int(getattr(character, "damroll", 0))
|
|
191
|
+
db_char.wimpy = int(getattr(character, "wimpy", 0))
|
|
192
|
+
db_char.position = int(getattr(character, "position", 8))
|
|
193
|
+
db_char.played = total_played
|
|
194
|
+
db_char.logon = logon_value
|
|
195
|
+
db_char.lines = int(getattr(character, "lines", 22))
|
|
196
|
+
db_char.prompt = getattr(character, "prompt", None)
|
|
197
|
+
prefix_value = getattr(character, "prefix", None)
|
|
198
|
+
db_char.prefix = str(prefix_value) if prefix_value is not None else None
|
|
199
|
+
db_char.affected_by = int(getattr(character, "affected_by", 0))
|
|
200
|
+
db_char.comm = int(getattr(character, "comm", 0))
|
|
201
|
+
db_char.wiznet = int(getattr(character, "wiznet", 0))
|
|
202
|
+
db_char.log_commands = bool(getattr(character, "log_commands", False))
|
|
203
|
+
db_char.newbie_help_seen = bool(getattr(character, "newbie_help_seen", False))
|
|
204
|
+
db_char.pfile_version = 1 # TABLES-001: always ROM-canonical on DB path
|
|
205
|
+
|
|
206
|
+
# --- pcdata scalar fields ---
|
|
207
|
+
db_char.title = getattr(pcdata, "title", None)
|
|
208
|
+
bamfin = getattr(pcdata, "bamfin", None)
|
|
209
|
+
db_char.bamfin = str(bamfin) if bamfin is not None else None
|
|
210
|
+
bamfout = getattr(pcdata, "bamfout", None)
|
|
211
|
+
db_char.bamfout = str(bamfout) if bamfout is not None else None
|
|
212
|
+
db_char.security = int(getattr(pcdata, "security", 0))
|
|
213
|
+
db_char.points = int(getattr(pcdata, "points", 0))
|
|
214
|
+
db_char.last_level = int(getattr(pcdata, "last_level", 0))
|
|
215
|
+
|
|
216
|
+
# --- password_hash sync ---
|
|
217
|
+
pwd = getattr(pcdata, "pwd", "") or ""
|
|
218
|
+
if pwd:
|
|
219
|
+
db_char.password_hash = pwd
|
|
220
|
+
|
|
221
|
+
# --- JSON collection fields ---
|
|
222
|
+
# skills: merge char.skills and pcdata.learned (mirroring persistence.py:898-901)
|
|
223
|
+
skills_snapshot = _serialize_skill_map(getattr(character, "skills", {}))
|
|
224
|
+
pcdata.learned = dict(skills_snapshot)
|
|
225
|
+
db_char.skills = skills_snapshot
|
|
226
|
+
|
|
227
|
+
# groups: from pcdata.group_known (mirroring persistence.py:899)
|
|
228
|
+
groups_snapshot = _serialize_groups(getattr(pcdata, "group_known", ()))
|
|
229
|
+
pcdata.group_known = tuple(groups_snapshot)
|
|
230
|
+
db_char.groups = groups_snapshot
|
|
231
|
+
|
|
232
|
+
# colours
|
|
233
|
+
colour_table = _serialize_colour_table(pcdata)
|
|
234
|
+
db_char.colours = colour_table
|
|
235
|
+
|
|
236
|
+
# conditions [drunk, full, thirst, hunger]
|
|
237
|
+
raw_conditions = list(getattr(pcdata, "condition", []))
|
|
238
|
+
conditions = [0, 48, 48, 48]
|
|
239
|
+
for idx, val in enumerate(raw_conditions[:4]):
|
|
240
|
+
try:
|
|
241
|
+
conditions[idx] = int(val)
|
|
242
|
+
except (TypeError, ValueError):
|
|
243
|
+
pass
|
|
244
|
+
db_char.conditions = conditions
|
|
245
|
+
|
|
246
|
+
# armor and mod_stat
|
|
247
|
+
db_char.armor = _normalize_int_list(getattr(character, "armor", []), 4)
|
|
248
|
+
db_char.mod_stat = _normalize_int_list(getattr(character, "mod_stat", []), 5)
|
|
249
|
+
|
|
250
|
+
# aliases
|
|
251
|
+
db_char.aliases = dict(getattr(character, "aliases", {}))
|
|
252
|
+
|
|
253
|
+
# board name
|
|
254
|
+
board_name = getattr(pcdata, "board_name", DEFAULT_BOARD_NAME) or DEFAULT_BOARD_NAME
|
|
255
|
+
db_char.board = board_name
|
|
256
|
+
|
|
257
|
+
# last_notes
|
|
258
|
+
db_char.last_notes = dict(getattr(pcdata, "last_notes", {}) or {})
|
|
259
|
+
|
|
260
|
+
# perm_stats JSON (already stored as string, keep existing column)
|
|
261
|
+
from mud.models.character import _encode_perm_stats
|
|
262
|
+
db_char.perm_stats = _encode_perm_stats(getattr(character, "perm_stat", []))
|
|
263
|
+
|
|
264
|
+
# creation_groups / creation_skills (keep in sync)
|
|
265
|
+
from mud.models.character import _encode_creation_groups, _encode_creation_skills
|
|
266
|
+
db_char.creation_groups = _encode_creation_groups(getattr(character, "creation_groups", ()))
|
|
267
|
+
db_char.creation_skills = _encode_creation_skills(getattr(character, "creation_skills", ()))
|
|
268
|
+
db_char.creation_points = int(getattr(character, "creation_points", 0) or 0)
|
|
269
|
+
|
|
270
|
+
# inventory as JSON blob (Option A from audit §3.1)
|
|
271
|
+
inventory_list = []
|
|
272
|
+
for obj in character.inventory:
|
|
273
|
+
try:
|
|
274
|
+
obj_save = _serialize_object(obj)
|
|
275
|
+
# Convert dataclass to dict for JSON storage
|
|
276
|
+
inventory_list.append(_dataclass_to_dict(obj_save))
|
|
277
|
+
except Exception:
|
|
278
|
+
pass
|
|
279
|
+
db_char.inventory_state = inventory_list
|
|
280
|
+
|
|
281
|
+
# equipment as JSON blob (Option A from audit §3.1)
|
|
282
|
+
equipment_dict = {}
|
|
283
|
+
for slot, obj in character.equipment.items():
|
|
284
|
+
try:
|
|
285
|
+
obj_save = _serialize_object(obj, wear_slot=slot)
|
|
286
|
+
equipment_dict[slot] = _dataclass_to_dict(obj_save)
|
|
287
|
+
except Exception:
|
|
288
|
+
pass
|
|
289
|
+
db_char.equipment_state = equipment_dict
|
|
290
|
+
|
|
291
|
+
# pet as JSON blob (audit §3.1)
|
|
292
|
+
pet = getattr(character, "pet", None)
|
|
293
|
+
if pet is not None:
|
|
294
|
+
try:
|
|
295
|
+
pet_save = _serialize_pet(pet)
|
|
296
|
+
if pet_save is not None:
|
|
297
|
+
db_char.pet_state = _dataclass_to_dict(pet_save)
|
|
298
|
+
else:
|
|
299
|
+
db_char.pet_state = None
|
|
300
|
+
except Exception:
|
|
301
|
+
db_char.pet_state = None
|
|
302
|
+
else:
|
|
303
|
+
db_char.pet_state = None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _dataclass_to_dict(obj: object) -> dict:
|
|
307
|
+
"""Recursively convert a dataclass instance to a plain dict for JSON storage."""
|
|
308
|
+
import dataclasses
|
|
309
|
+
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
|
|
310
|
+
result = {}
|
|
311
|
+
for f in dataclasses.fields(obj): # type: ignore[arg-type]
|
|
312
|
+
val = getattr(obj, f.name)
|
|
313
|
+
if dataclasses.is_dataclass(val) and not isinstance(val, type):
|
|
314
|
+
result[f.name] = _dataclass_to_dict(val)
|
|
315
|
+
elif isinstance(val, list):
|
|
316
|
+
result[f.name] = [
|
|
317
|
+
_dataclass_to_dict(item) if (dataclasses.is_dataclass(item) and not isinstance(item, type)) else item
|
|
318
|
+
for item in val
|
|
319
|
+
]
|
|
320
|
+
elif isinstance(val, dict):
|
|
321
|
+
result[f.name] = {
|
|
322
|
+
k: (_dataclass_to_dict(v) if (dataclasses.is_dataclass(v) and not isinstance(v, type)) else v)
|
|
323
|
+
for k, v in val.items()
|
|
324
|
+
}
|
|
325
|
+
else:
|
|
326
|
+
result[f.name] = val
|
|
327
|
+
return result
|
|
328
|
+
return obj # type: ignore[return-value]
|