Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
364 changes: 226 additions & 138 deletions PYTHON_PORT_PLAN.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions mud/account/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
create_character,
clear_active_accounts,
is_account_active,
is_valid_account_name,
LoginFailureReason,
LoginResult,
list_characters,
login,
login_with_host,
sanitize_account_name,
release_account,
)

Expand All @@ -22,6 +24,8 @@
"login_with_host",
"list_characters",
"create_character",
"is_valid_account_name",
"sanitize_account_name",
"clear_active_accounts",
"is_account_active",
"release_account",
Expand Down
75 changes: 75 additions & 0 deletions mud/account/account_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,71 @@ class LoginResult(NamedTuple):

_active_accounts: set[str] = set()

_RESERVED_NAMES = {
"all",
"auto",
"immortal",
"self",
"someone",
"something",
"the",
"you",
"loner",
"none",
}


def sanitize_account_name(username: str) -> str:
"""Trim surrounding whitespace from a submitted account name."""

return username.strip()


def is_valid_account_name(username: str) -> bool:
"""Return True when the candidate matches ROM's `check_parse_name`."""

candidate = sanitize_account_name(username)
if not candidate:
return False

lowered = candidate.lower()
if lowered in _RESERVED_NAMES:
return False

capitalized = candidate.capitalize()
if capitalized != "Alander" and (
capitalized.startswith("Alan") or capitalized.endswith("Alander")
):
return False

if len(candidate) < 2 or len(candidate) > 12:
return False

f_ill = True
adjcaps = False
cleancaps = False
total_caps = 0
for char in candidate:
if not char.isalpha():
return False
if char.isupper():
if adjcaps:
cleancaps = True
total_caps += 1
adjcaps = True
else:
adjcaps = False
if char.lower() not in {"i", "l"}:
f_ill = False

if f_ill:
return False

if cleancaps or (total_caps > len(candidate) // 2 and len(candidate) < 3):
return False

return True


def _normalize(username: str) -> str:
return username.strip().lower()
Expand Down Expand Up @@ -74,6 +139,9 @@ def is_account_active(username: str) -> bool:

def create_account(username: str, raw_password: str) -> bool:
"""Create a new PlayerAccount if username is available."""
username = sanitize_account_name(username)
if not is_valid_account_name(username):
return False
if is_newlock_enabled():
return False
session = SessionLocal()
Expand All @@ -92,6 +160,9 @@ def create_account(username: str, raw_password: str) -> bool:

def login(username: str, raw_password: str) -> PlayerAccount | None:
"""Return PlayerAccount if credentials match."""
username = sanitize_account_name(username)
if not username:
return None
# Enforce account-name bans irrespective of host.
if bans.is_account_banned(username):
return None
Expand Down Expand Up @@ -121,6 +192,10 @@ def login_with_host(
nanny prompts.
"""

username = sanitize_account_name(username)
if not is_valid_account_name(username):
return LoginResult(None, LoginFailureReason.UNKNOWN_ACCOUNT)

if bans.is_account_banned(username):
return LoginResult(None, LoginFailureReason.ACCOUNT_BANNED)

Expand Down
3 changes: 3 additions & 0 deletions mud/commands/movement.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ def do_enter(char: Character, args: str = "") -> str:
if not target:
return "Enter what?"

if getattr(char, "fighting", None) is not None:
return "No way! You are still fighting!"

# Find a portal object in the room matching target token
portal = None
for obj in getattr(char.room, "contents", []):
Expand Down
14 changes: 14 additions & 0 deletions mud/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@ def get_pulse_area() -> int:
return max(1, base // scale)


def get_pulse_music() -> int:
"""Return pulses per music update (ROM PULSE_MUSIC)."""

scale = max(1, int(os.getenv("TIME_SCALE", os.getenv("MUD_TIME_SCALE", "1")) or 1))
try:
from mud import config as _cfg

scale = max(scale, int(getattr(_cfg, "TIME_SCALE", 1)))
except Exception:
pass
base = 6 * PULSE_PER_SECOND
return max(1, base // scale)


# Feature flags
COMBAT_USE_THAC0: bool = False

Expand Down
10 changes: 8 additions & 2 deletions mud/game_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from mud.ai import aggressive_update
from mud.characters.conditions import gain_condition
from mud.combat.engine import update_pos
from mud.config import get_pulse_area, get_pulse_tick, get_pulse_violence
from mud.config import get_pulse_area, get_pulse_music, get_pulse_tick, get_pulse_violence
from mud.imc import pump_idle
from mud.math.c_compat import c_div
from mud.admin_logging.admin import rotate_admin_log
Expand All @@ -28,6 +28,7 @@
from mud.models.obj import ObjectData, object_registry
from mud.models.room import room_registry
from mud.net.protocol import broadcast_global
from mud.music import song_update
from mud.skills.registry import skill_registry
from mud.spawning.reset_handler import reset_tick
from mud.spec_funs import run_npc_specs
Expand Down Expand Up @@ -597,6 +598,7 @@ def time_tick() -> None:
_point_counter = get_pulse_tick()
_violence_counter = get_pulse_violence()
_area_counter = get_pulse_area()
_music_counter = get_pulse_music()


def violence_tick() -> None:
Expand Down Expand Up @@ -633,7 +635,7 @@ def _mobprog_idle_tick() -> None:

def game_tick() -> None:
"""Run a full game tick: time, regen, weather, timed events, and resets."""
global _pulse_counter, _point_counter, _violence_counter, _area_counter
global _pulse_counter, _point_counter, _violence_counter, _area_counter, _music_counter
_pulse_counter += 1

# Consume wait/daze every pulse before evaluating cadence counters.
Expand All @@ -659,6 +661,10 @@ def game_tick() -> None:
if _area_counter <= 0:
_area_counter = get_pulse_area()
reset_tick()
_music_counter -= 1
if _music_counter <= 0:
_music_counter = get_pulse_music()
song_update()
event_tick()
_mobprog_idle_tick()
aggressive_update()
Expand Down
22 changes: 22 additions & 0 deletions mud/imc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@

import os

from .commands import (
IMCCommand,
IMCPacket,
PacketHandler,
build_default_packet_handlers,
load_command_table,
)

@dataclass(frozen=True)
class IMCChannel:
Expand Down Expand Up @@ -35,12 +42,20 @@ class IMCState:
config: Dict[str, str]
channels: List[IMCChannel]
helps: Dict[str, IMCHelp]
commands: Dict[str, IMCCommand]
packet_handlers: Dict[str, PacketHandler]
connected: bool
config_path: Path
channels_path: Path
help_path: Path
commands_path: Path
idle_pulses: int = 0

def dispatch_packet(self, packet: IMCPacket) -> None:
handler = self.packet_handlers.get(packet.type)
if handler:
handler(packet)


_state: Optional[IMCState] = None

Expand Down Expand Up @@ -227,6 +242,7 @@ def maybe_open_socket(force_reload: bool = False) -> Optional[IMCState]:
config_path = _resolve_path("IMC_CONFIG_PATH", "imc.config")
channels_path = _resolve_path("IMC_CHANNELS_PATH", "imc.channels")
help_path = _resolve_path("IMC_HELP_PATH", "imc.help")
commands_path = _resolve_path("IMC_COMMANDS_PATH", "imc.commands")

if (
_state
Expand All @@ -235,22 +251,28 @@ def maybe_open_socket(force_reload: bool = False) -> Optional[IMCState]:
and _state.config_path == config_path
and _state.channels_path == channels_path
and _state.help_path == help_path
and _state.commands_path == commands_path
):
return _state

commands = load_command_table(commands_path)
config = _parse_config(config_path)
channels = _parse_channels(channels_path)
helps = _parse_helps(help_path)
packet_handlers = build_default_packet_handlers()

idle_pulses = _state.idle_pulses if _state else 0
_state = IMCState(
config=config,
channels=channels,
helps=helps,
commands=commands,
packet_handlers=packet_handlers,
connected=True,
config_path=config_path,
channels_path=channels_path,
help_path=help_path,
commands_path=commands_path,
idle_pulses=idle_pulses,
)
return _state
Expand Down
Loading
Loading