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/db/models.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean
|
|
2
|
+
from sqlalchemy.orm import relationship, declarative_base
|
|
3
|
+
|
|
4
|
+
Base = declarative_base()
|
|
5
|
+
|
|
6
|
+
class Area(Base):
|
|
7
|
+
__tablename__ = "areas"
|
|
8
|
+
id = Column(Integer, primary_key=True)
|
|
9
|
+
vnum = Column(Integer, unique=True)
|
|
10
|
+
name = Column(String)
|
|
11
|
+
min_vnum = Column(Integer)
|
|
12
|
+
max_vnum = Column(Integer)
|
|
13
|
+
rooms = relationship("Room", back_populates="area")
|
|
14
|
+
|
|
15
|
+
class Room(Base):
|
|
16
|
+
__tablename__ = "rooms"
|
|
17
|
+
id = Column(Integer, primary_key=True)
|
|
18
|
+
vnum = Column(Integer, unique=True)
|
|
19
|
+
name = Column(String)
|
|
20
|
+
description = Column(String)
|
|
21
|
+
sector_type = Column(Integer)
|
|
22
|
+
room_flags = Column(Integer)
|
|
23
|
+
area_id = Column(Integer, ForeignKey("areas.id"))
|
|
24
|
+
area = relationship("Area", back_populates="rooms")
|
|
25
|
+
exits = relationship("Exit", back_populates="room")
|
|
26
|
+
|
|
27
|
+
class Exit(Base):
|
|
28
|
+
__tablename__ = "exits"
|
|
29
|
+
id = Column(Integer, primary_key=True)
|
|
30
|
+
room_id = Column(Integer, ForeignKey("rooms.id"))
|
|
31
|
+
direction = Column(String)
|
|
32
|
+
to_room_vnum = Column(Integer)
|
|
33
|
+
room = relationship("Room", back_populates="exits")
|
|
34
|
+
|
|
35
|
+
class MobPrototype(Base):
|
|
36
|
+
__tablename__ = "mob_prototypes"
|
|
37
|
+
id = Column(Integer, primary_key=True)
|
|
38
|
+
vnum = Column(Integer, unique=True)
|
|
39
|
+
name = Column(String)
|
|
40
|
+
short_desc = Column(String)
|
|
41
|
+
long_desc = Column(String)
|
|
42
|
+
level = Column(Integer)
|
|
43
|
+
alignment = Column(Integer)
|
|
44
|
+
|
|
45
|
+
class ObjPrototype(Base):
|
|
46
|
+
__tablename__ = "obj_prototypes"
|
|
47
|
+
id = Column(Integer, primary_key=True)
|
|
48
|
+
vnum = Column(Integer, unique=True)
|
|
49
|
+
name = Column(String)
|
|
50
|
+
short_desc = Column(String)
|
|
51
|
+
long_desc = Column(String)
|
|
52
|
+
item_type = Column(Integer)
|
|
53
|
+
flags = Column(Integer)
|
|
54
|
+
value0 = Column(Integer)
|
|
55
|
+
value1 = Column(Integer)
|
|
56
|
+
value2 = Column(Integer)
|
|
57
|
+
value3 = Column(Integer)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ObjectInstance(Base):
|
|
61
|
+
__tablename__ = "object_instances"
|
|
62
|
+
id = Column(Integer, primary_key=True)
|
|
63
|
+
prototype_vnum = Column(Integer, ForeignKey("obj_prototypes.vnum"))
|
|
64
|
+
location = Column(String)
|
|
65
|
+
character_id = Column(Integer, ForeignKey("characters.id"))
|
|
66
|
+
|
|
67
|
+
prototype = relationship("ObjPrototype")
|
|
68
|
+
character = relationship("Character", back_populates="objects")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class PlayerAccount(Base):
|
|
72
|
+
__tablename__ = "player_accounts"
|
|
73
|
+
id = Column(Integer, primary_key=True)
|
|
74
|
+
username = Column(String, unique=True)
|
|
75
|
+
email = Column(String)
|
|
76
|
+
password_hash = Column(String)
|
|
77
|
+
is_admin = Column(Boolean, default=False)
|
|
78
|
+
|
|
79
|
+
characters = relationship("Character", back_populates="player")
|
|
80
|
+
|
|
81
|
+
def set_password(self, password: str):
|
|
82
|
+
"""Set the password hash for this account."""
|
|
83
|
+
from mud.security.hash_utils import hash_password
|
|
84
|
+
self.password_hash = hash_password(password)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Character(Base):
|
|
88
|
+
__tablename__ = "characters"
|
|
89
|
+
id = Column(Integer, primary_key=True)
|
|
90
|
+
name = Column(String, unique=True)
|
|
91
|
+
level = Column(Integer)
|
|
92
|
+
hp = Column(Integer)
|
|
93
|
+
room_vnum = Column(Integer)
|
|
94
|
+
|
|
95
|
+
player_id = Column(Integer, ForeignKey("player_accounts.id"))
|
|
96
|
+
player = relationship("PlayerAccount", back_populates="characters")
|
|
97
|
+
objects = relationship("ObjectInstance", back_populates="character")
|
|
98
|
+
|
mud/db/seed.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from mud.db.session import SessionLocal
|
|
4
|
+
from mud.db.models import PlayerAccount, Character
|
|
5
|
+
from mud.security.hash_utils import hash_password
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_test_account():
|
|
9
|
+
session = SessionLocal()
|
|
10
|
+
if session.query(PlayerAccount).filter_by(username="admin").first():
|
|
11
|
+
session.close()
|
|
12
|
+
return
|
|
13
|
+
account = PlayerAccount(
|
|
14
|
+
username="admin",
|
|
15
|
+
password_hash=hash_password("admin"),
|
|
16
|
+
is_admin=True,
|
|
17
|
+
)
|
|
18
|
+
char = Character(
|
|
19
|
+
name="Testman",
|
|
20
|
+
level=1,
|
|
21
|
+
hp=100,
|
|
22
|
+
room_vnum=3001,
|
|
23
|
+
player=account,
|
|
24
|
+
)
|
|
25
|
+
session.add(account)
|
|
26
|
+
session.add(char)
|
|
27
|
+
session.commit()
|
|
28
|
+
session.close()
|
mud/db/session.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from sqlalchemy import create_engine
|
|
3
|
+
from sqlalchemy.orm import sessionmaker
|
|
4
|
+
|
|
5
|
+
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///mud.db")
|
|
6
|
+
|
|
7
|
+
engine = create_engine(
|
|
8
|
+
DATABASE_URL,
|
|
9
|
+
connect_args={"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {},
|
|
10
|
+
)
|
|
11
|
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
mud/devtools/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from mud.registry import room_registry, mob_registry
|
|
2
|
+
from mud.spawning.mob_spawner import spawn_mob
|
|
3
|
+
from mud.agent.character_agent import CharacterAgentAdapter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run_agent_demo() -> None:
|
|
7
|
+
room = room_registry.get(3001)
|
|
8
|
+
proto_vnum = next(iter(mob_registry)) if mob_registry else None
|
|
9
|
+
if proto_vnum is None or room is None:
|
|
10
|
+
print("World not initialized or no mobs available")
|
|
11
|
+
return
|
|
12
|
+
mob = spawn_mob(proto_vnum)
|
|
13
|
+
if not mob:
|
|
14
|
+
print("Failed to spawn mob")
|
|
15
|
+
return
|
|
16
|
+
adapter = CharacterAgentAdapter(mob)
|
|
17
|
+
room.add_mob(mob)
|
|
18
|
+
print(adapter.get_observation())
|
|
19
|
+
print(adapter.perform_action("say", ["I", "am", "alive!"]))
|
mud/entrypoint.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from mud.account.account_service import create_account, login
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def prompt_login():
|
|
7
|
+
print("Welcome to the Realm.")
|
|
8
|
+
username = input("Username: ")
|
|
9
|
+
password = input("Password: ")
|
|
10
|
+
|
|
11
|
+
account = login(username, password)
|
|
12
|
+
if not account:
|
|
13
|
+
print("❌ Invalid login.")
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
print(f"✅ Logged in as {username}")
|
|
17
|
+
return account
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def prompt_account_creation():
|
|
21
|
+
print("Create your account:")
|
|
22
|
+
username = input("Username: ")
|
|
23
|
+
password = input("Password: ")
|
|
24
|
+
confirm = input("Confirm Password: ")
|
|
25
|
+
if password != confirm:
|
|
26
|
+
print("❌ Passwords do not match.")
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
success = create_account(username, password)
|
|
30
|
+
if success:
|
|
31
|
+
print("✅ Account created.")
|
|
32
|
+
else:
|
|
33
|
+
print("❌ Username already taken.")
|
|
34
|
+
return success
|
mud/game_loop.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Callable, List
|
|
5
|
+
|
|
6
|
+
from mud.models.character import Character, character_registry
|
|
7
|
+
from mud.skills.registry import skill_registry
|
|
8
|
+
from mud.spawning.reset_handler import reset_tick
|
|
9
|
+
from mud.time import time_info
|
|
10
|
+
from mud.config import get_pulse_tick, get_pulse_violence, GAME_LOOP_STRICT_POINT
|
|
11
|
+
from mud.net.protocol import broadcast_global
|
|
12
|
+
from mud.logging.admin import rotate_admin_log
|
|
13
|
+
from mud.spec_funs import run_npc_specs
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class WeatherState:
|
|
18
|
+
"""Very small placeholder for global weather."""
|
|
19
|
+
sky: str = "sunny"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
weather = WeatherState()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class TimedEvent:
|
|
27
|
+
ticks: int
|
|
28
|
+
callback: Callable[[], None]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
events: List[TimedEvent] = []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def schedule_event(ticks: int, callback: Callable[[], None]) -> None:
|
|
35
|
+
"""Schedule a callback to run after a number of ticks."""
|
|
36
|
+
events.append(TimedEvent(ticks, callback))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def event_tick() -> None:
|
|
40
|
+
"""Advance timers and fire ready callbacks."""
|
|
41
|
+
for ev in events[:]:
|
|
42
|
+
ev.ticks -= 1
|
|
43
|
+
if ev.ticks <= 0:
|
|
44
|
+
ev.callback()
|
|
45
|
+
events.remove(ev)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def regen_character(ch: Character) -> None:
|
|
49
|
+
"""Apply a single tick of regeneration to a character."""
|
|
50
|
+
ch.hit = min(ch.max_hit, ch.hit + 1)
|
|
51
|
+
ch.mana = min(ch.max_mana, ch.mana + 1)
|
|
52
|
+
ch.move = min(ch.max_move, ch.move + 1)
|
|
53
|
+
skill_registry.tick(ch)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def regen_tick() -> None:
|
|
57
|
+
for ch in list(character_registry):
|
|
58
|
+
regen_character(ch)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_WEATHER_STATES = ["sunny", "cloudy", "rainy"]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def weather_tick() -> None:
|
|
65
|
+
"""Cycle through simple weather states."""
|
|
66
|
+
index = _WEATHER_STATES.index(weather.sky)
|
|
67
|
+
weather.sky = _WEATHER_STATES[(index + 1) % len(_WEATHER_STATES)]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def time_tick() -> None:
|
|
71
|
+
"""Advance world time and broadcast day/night transitions."""
|
|
72
|
+
messages = time_info.advance_hour()
|
|
73
|
+
if time_info.hour == 0:
|
|
74
|
+
try:
|
|
75
|
+
rotate_admin_log()
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
for message in messages:
|
|
79
|
+
broadcast_global(message, channel="info")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
_pulse_counter = 0
|
|
83
|
+
_violence_counter = 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def violence_tick() -> None:
|
|
87
|
+
"""Violence cadence updates: decrement wait/daze counters."""
|
|
88
|
+
for ch in list(character_registry):
|
|
89
|
+
ch.wait = max(0, int(getattr(ch, "wait", 0)) - 1)
|
|
90
|
+
# daze is optional in some tests; default to attr when present
|
|
91
|
+
if hasattr(ch, "daze"):
|
|
92
|
+
ch.daze = max(0, int(getattr(ch, "daze", 0)) - 1)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def game_tick() -> None:
|
|
96
|
+
"""Run a full game tick: time, regen, weather, timed events, and resets."""
|
|
97
|
+
global _pulse_counter, _violence_counter
|
|
98
|
+
_pulse_counter += 1
|
|
99
|
+
# Violence cadence: decrement wait/daze on PULSE_VIOLENCE boundaries
|
|
100
|
+
# This mirrors ROM's update_handler calling violence_update.
|
|
101
|
+
_violence_counter += 1
|
|
102
|
+
if _violence_counter % get_pulse_violence() == 0:
|
|
103
|
+
violence_tick()
|
|
104
|
+
# Advance time/weather/resets on point pulses, preserving legacy behavior when not strict
|
|
105
|
+
point_pulse = (_pulse_counter % get_pulse_tick() == 0)
|
|
106
|
+
if point_pulse:
|
|
107
|
+
time_tick()
|
|
108
|
+
weather_tick()
|
|
109
|
+
reset_tick()
|
|
110
|
+
else:
|
|
111
|
+
if not GAME_LOOP_STRICT_POINT:
|
|
112
|
+
weather_tick()
|
|
113
|
+
reset_tick()
|
|
114
|
+
regen_tick()
|
|
115
|
+
event_tick()
|
|
116
|
+
# Invoke NPC special functions after resets to mirror ROM's update cadence
|
|
117
|
+
run_npc_specs()
|
mud/imc/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def imc_enabled() -> bool:
|
|
7
|
+
"""Feature flag for IMC. Disabled by default."""
|
|
8
|
+
return os.getenv("IMC_ENABLED", "false").lower() in {"1", "true", "yes"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def maybe_open_socket() -> None:
|
|
12
|
+
"""No-op when IMC is disabled. Never opens sockets in disabled mode."""
|
|
13
|
+
if not imc_enabled():
|
|
14
|
+
return None
|
|
15
|
+
# Intentionally unimplemented: networking is out of scope for P0 stub.
|
|
16
|
+
raise NotImplementedError("IMC networking not implemented in stub")
|
|
17
|
+
|
mud/imc/protocol.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class Frame:
|
|
8
|
+
"""Minimal IMC-like frame for testing parser/serializer.
|
|
9
|
+
|
|
10
|
+
Format (text line):
|
|
11
|
+
"<type> <from> <to> :<message>"
|
|
12
|
+
Example:
|
|
13
|
+
"chat alice@quickmud * :Hello world"
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
type: str
|
|
17
|
+
source: str
|
|
18
|
+
target: str
|
|
19
|
+
message: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_frame(line: str) -> Frame:
|
|
23
|
+
parts = line.strip().split(" ", 3)
|
|
24
|
+
if len(parts) < 4 or not parts[3].startswith(":"):
|
|
25
|
+
raise ValueError("invalid IMC frame: expected '<type> <from> <to> :<message>'")
|
|
26
|
+
ftype, source, target, msg = parts
|
|
27
|
+
return Frame(type=ftype, source=source, target=target, message=msg[1:])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def serialize_frame(frame: Frame) -> str:
|
|
31
|
+
return f"{frame.type} {frame.source} {frame.target} :{frame.message}"
|
|
32
|
+
|
mud/loaders/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .area_loader import load_area_file
|
|
2
|
+
from .json_area_loader import load_all_areas_from_json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from mud.registry import area_registry, room_registry, mob_registry, obj_registry, shop_registry
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_all_areas(list_path: str = "area/area.lst"):
|
|
9
|
+
# Clear all registries before loading to prevent conflicts
|
|
10
|
+
area_registry.clear()
|
|
11
|
+
room_registry.clear()
|
|
12
|
+
mob_registry.clear()
|
|
13
|
+
obj_registry.clear()
|
|
14
|
+
shop_registry.clear()
|
|
15
|
+
sentinel_found = False
|
|
16
|
+
with open(list_path, 'r', encoding='latin-1') as f:
|
|
17
|
+
for line in f:
|
|
18
|
+
line = line.strip()
|
|
19
|
+
if not line or line.startswith('#'):
|
|
20
|
+
continue
|
|
21
|
+
if line == '$':
|
|
22
|
+
sentinel_found = True
|
|
23
|
+
break
|
|
24
|
+
path = Path('area') / line
|
|
25
|
+
load_area_file(str(path))
|
|
26
|
+
if not sentinel_found:
|
|
27
|
+
raise ValueError("area.lst missing '$' sentinel")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from .base_loader import BaseTokenizer
|
|
2
|
+
from .room_loader import load_rooms
|
|
3
|
+
from .mob_loader import load_mobiles
|
|
4
|
+
from .obj_loader import load_objects
|
|
5
|
+
from .reset_loader import load_resets
|
|
6
|
+
from .shop_loader import load_shops
|
|
7
|
+
from .specials_loader import load_specials
|
|
8
|
+
from mud.models.area import Area
|
|
9
|
+
from mud.registry import area_registry
|
|
10
|
+
|
|
11
|
+
SECTION_HANDLERS = {
|
|
12
|
+
"#ROOMS": load_rooms,
|
|
13
|
+
"#MOBILES": load_mobiles,
|
|
14
|
+
"#OBJECTS": load_objects,
|
|
15
|
+
"#RESETS": load_resets,
|
|
16
|
+
"#SHOPS": load_shops,
|
|
17
|
+
"#SPECIALS": load_specials,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_area_file(filepath: str) -> Area:
|
|
22
|
+
with open(filepath, 'r', encoding='latin-1') as f:
|
|
23
|
+
lines = f.readlines()
|
|
24
|
+
tokenizer = BaseTokenizer(lines)
|
|
25
|
+
area = Area()
|
|
26
|
+
while True:
|
|
27
|
+
line = tokenizer.next_line()
|
|
28
|
+
if line is None:
|
|
29
|
+
break
|
|
30
|
+
if line == '#AREA':
|
|
31
|
+
area.file_name = tokenizer.next_line().rstrip('~')
|
|
32
|
+
area.name = tokenizer.next_line().rstrip('~')
|
|
33
|
+
area.credits = tokenizer.next_line().rstrip('~')
|
|
34
|
+
vnums = tokenizer.next_line()
|
|
35
|
+
if vnums:
|
|
36
|
+
parts = vnums.split()
|
|
37
|
+
if len(parts) >= 2:
|
|
38
|
+
area.min_vnum = int(parts[0])
|
|
39
|
+
area.max_vnum = int(parts[1])
|
|
40
|
+
elif line in SECTION_HANDLERS:
|
|
41
|
+
handler = SECTION_HANDLERS[line]
|
|
42
|
+
handler(tokenizer, area)
|
|
43
|
+
elif line == "#AREADATA":
|
|
44
|
+
while True:
|
|
45
|
+
peek = tokenizer.peek_line()
|
|
46
|
+
if peek is None or peek.startswith('#'):
|
|
47
|
+
break
|
|
48
|
+
data_line = tokenizer.next_line()
|
|
49
|
+
if data_line.startswith('Builders'):
|
|
50
|
+
area.builders = data_line.split(None, 1)[1].rstrip('~')
|
|
51
|
+
elif data_line.startswith('Security'):
|
|
52
|
+
parts = data_line.split()
|
|
53
|
+
if len(parts) > 1:
|
|
54
|
+
area.security = int(parts[1])
|
|
55
|
+
elif data_line.startswith('Flags'):
|
|
56
|
+
parts = data_line.split()
|
|
57
|
+
if len(parts) > 1:
|
|
58
|
+
area.area_flags = int(parts[1])
|
|
59
|
+
elif line.startswith('#$') or line == '$':
|
|
60
|
+
break
|
|
61
|
+
key = area.min_vnum
|
|
62
|
+
area.vnum = area.min_vnum
|
|
63
|
+
# START enforce unique area vnum
|
|
64
|
+
if (
|
|
65
|
+
key != 0
|
|
66
|
+
and key in area_registry
|
|
67
|
+
and area_registry[key].file_name != area.file_name
|
|
68
|
+
):
|
|
69
|
+
raise ValueError(f"duplicate area vnum {key}")
|
|
70
|
+
# END enforce unique area vnum
|
|
71
|
+
area_registry[key] = area
|
|
72
|
+
|
|
73
|
+
return area
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class BaseTokenizer:
|
|
2
|
+
"""Simple tokenizer for area files."""
|
|
3
|
+
def __init__(self, lines):
|
|
4
|
+
self.lines = [line.rstrip('\n') for line in lines]
|
|
5
|
+
self.index = 0
|
|
6
|
+
|
|
7
|
+
def next_line(self):
|
|
8
|
+
while self.index < len(self.lines):
|
|
9
|
+
line = self.lines[self.index].strip()
|
|
10
|
+
self.index += 1
|
|
11
|
+
if line.startswith('*') or line == '':
|
|
12
|
+
continue
|
|
13
|
+
return line
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
def peek_line(self):
|
|
17
|
+
pos = self.index
|
|
18
|
+
line = self.next_line()
|
|
19
|
+
self.index = pos
|
|
20
|
+
return line
|
|
21
|
+
|
|
22
|
+
def read_string_tilde(self):
|
|
23
|
+
parts = []
|
|
24
|
+
while True:
|
|
25
|
+
line = self.next_line()
|
|
26
|
+
if line is None:
|
|
27
|
+
break
|
|
28
|
+
if line.endswith('~'):
|
|
29
|
+
parts.append(line[:-1])
|
|
30
|
+
break
|
|
31
|
+
parts.append(line)
|
|
32
|
+
return '\n'.join(parts)
|
|
33
|
+
|
|
34
|
+
def read_number(self):
|
|
35
|
+
line = self.next_line()
|
|
36
|
+
if line is None:
|
|
37
|
+
raise ValueError('Unexpected EOF while reading number')
|
|
38
|
+
return int(line.split()[0])
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from mud.models.help import HelpEntry, register_help, help_registry
|
|
7
|
+
from mud.models.help_json import HelpJson
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_help_file(path: str | Path) -> None:
|
|
11
|
+
"""Load help entries from ``path`` into ``help_registry``."""
|
|
12
|
+
with open(path, "r", encoding="utf-8") as fp:
|
|
13
|
+
data = json.load(fp)
|
|
14
|
+
help_registry.clear()
|
|
15
|
+
for raw in data:
|
|
16
|
+
entry = HelpEntry.from_json(HelpJson.from_dict(raw))
|
|
17
|
+
register_help(entry)
|