#!/usr/bin/env python """ Timmy Academy - World Rebuild Script (Second Pass) Resets all room descriptions, typeclasses, and attributes from wing module source files. Sets wing membership, verifies exits, moves characters to Limbo, and configures the Public channel. Safe to rerun (idempotent). Usage: cd /root/workspace/timmy-academy source /root/workspace/evennia-venv/bin/activate python world/rebuild_world.py """ import os import sys import re import ast os.environ["DJANGO_SETTINGS_MODULE"] = "server.conf.settings" sys.path.insert(0, "/root/workspace/timmy-academy") import django django.setup() from evennia.objects.models import ObjectDB from evennia.comms.models import ChannelDB BANNER = """ ============================================================ TIMMY ACADEMY - WORLD REBUILD (Second Pass) Applying rich descriptions, typeclasses, atmospheres ============================================================ """ WORLD_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__))) def parse_wing_file(filename): """Parse a wing .py file and extract class attributes by class name. Returns dict: { ClassName: { desc, atmosphere, objects, aliases, exits, features } } """ filepath = os.path.join(WORLD_DIR, filename) with open(filepath, "r") as f: source = f.read() results = {} # Split by class definitions class_pattern = re.compile(r'^class\s+(\w+)\(', re.MULTILINE) class_positions = [(m.group(1), m.start()) for m in class_pattern.finditer(source)] for i, (cls_name, start_pos) in enumerate(class_positions): # Get the source for this class (up to next class or end) if i + 1 < len(class_positions): end_pos = class_positions[i + 1][1] else: end_pos = len(source) cls_source = source[start_pos:end_pos] attrs = {} # Extract self.db.desc = """...""" desc_match = re.search( r'self\.db\.desc\s*=\s*"""(.*?)"""', cls_source, re.DOTALL ) if desc_match: desc = desc_match.group(1) # Clean up leading/trailing whitespace while preserving internal formatting lines = desc.split('\n') # Remove common leading whitespace non_empty = [l for l in lines if l.strip()] if non_empty: min_indent = min(len(l) - len(l.lstrip()) for l in non_empty) lines = [l[min_indent:] if len(l) >= min_indent else l for l in lines] desc = '\n'.join(lines).strip() attrs["desc"] = desc # Extract self.db.atmosphere = { ... } atmo_match = re.search( r'self\.db\.atmosphere\s*=\s*(\{[^}]+\})', cls_source, re.DOTALL ) if atmo_match: try: attrs["atmosphere"] = ast.literal_eval(atmo_match.group(1)) except Exception: pass # Extract self.db.objects = [ ... ] objs_match = re.search( r'self\.db\.objects\s*=\s*(\[[^\]]+\])', cls_source, re.DOTALL ) if objs_match: try: attrs["objects"] = ast.literal_eval(objs_match.group(1)) except Exception: pass # Extract self.db.exits = { ... } exits_match = re.search( r'self\.db\.exits\s*=\s*(\{[^}]+\})', cls_source, re.DOTALL ) if exits_match: try: attrs["exits_info"] = ast.literal_eval(exits_match.group(1)) except Exception: pass # Extract self.aliases = [ ... ] aliases_match = re.search( r'self\.aliases\s*=\s*(\[[^\]]+\])', cls_source, re.DOTALL ) if aliases_match: try: attrs["aliases"] = ast.literal_eval(aliases_match.group(1)) except Exception: pass # Extract self.db.features = { ... } feats_match = re.search( r'self\.db\.features\s*=\s*(\{[^}]+\})', cls_source, re.DOTALL ) if feats_match: try: attrs["features"] = ast.literal_eval(feats_match.group(1)) except Exception: pass results[cls_name] = attrs return results # ============================================================ # Room ID -> (typeclass_path, wing_name, source_file, class_name) # ============================================================ ROOM_CONFIG = { # Dormitory Wing 3: ("world.dormitory_entrance.DormitoryEntrance", "dormitory", "dormitory_entrance.py", "DormitoryEntrance"), 4: ("world.dormitory_entrance.DormitoryLodgingMaster", "dormitory", "dormitory_entrance.py", "DormitoryLodgingMaster"), 5: ("world.dormitory_entrance.CorridorOfRest", "dormitory", "dormitory_entrance.py", "CorridorOfRest"), 6: ("world.dormitory_entrance.NoviceDormitoryHall", "dormitory", "dormitory_entrance.py", "NoviceDormitoryHall"), 7: ("world.dormitory_entrance.MasterSuitesApproach", "dormitory", "dormitory_entrance.py", "MasterSuitesApproach"), # Commons Wing 8: ("world.commons_wing.GrandCommonsHall", "commons", "commons_wing.py", "GrandCommonsHall"), 9: ("world.commons_wing.HearthsideDining", "commons", "commons_wing.py", "HearthsideDining"), 10: ("world.commons_wing.ScholarsCorner", "commons", "commons_wing.py", "ScholarsCorner"), 11: ("world.commons_wing.EntertainmentGallery", "commons", "commons_wing.py", "EntertainmentGallery"), 12: ("world.commons_wing.UpperCommonsBalcony", "commons", "commons_wing.py", "UpperCommonsBalcony"), # Workshop Wing 13: ("world.workshop_wing.WorkshopEntrance", "workshop", "workshop_wing.py", "WorkshopEntrance"), 14: ("world.workshop_wing.GreatSmithy", "workshop", "workshop_wing.py", "GreatSmithy"), 15: ("world.workshop_wing.AlchemyLaboratories", "workshop", "workshop_wing.py", "AlchemyLaboratories"), 16: ("world.workshop_wing.WoodworkingStudio", "workshop", "workshop_wing.py", "WoodworkingStudio"), 17: ("world.workshop_wing.ArtificingChambers", "workshop", "workshop_wing.py", "ArtificingChambers"), # Gardens Wing 18: ("world.gardens_wing.GardensEntrance", "gardens", "gardens_wing.py", "GardensEntrance"), 19: ("world.gardens_wing.HerbGardens", "gardens", "gardens_wing.py", "HerbGardens"), 20: ("world.gardens_wing.EnchantedGrove", "gardens", "gardens_wing.py", "EnchantedGrove"), 21: ("world.gardens_wing.GreenhouseComplex", "gardens", "gardens_wing.py", "GreenhouseComplex"), 22: ("world.gardens_wing.SacredGrove", "gardens", "gardens_wing.py", "SacredGrove"), } # Wing metadata WING_INFO = { "hub": { "name": "Academy Central Hub", "color": "|w", "rooms": [2], }, "dormitory": { "name": "Dormitory Wing", "color": "|c", "rooms": [3, 4, 5, 6, 7], }, "commons": { "name": "Commons Wing", "color": "|y", "rooms": [8, 9, 10, 11, 12], }, "workshop": { "name": "Workshop Wing", "color": "|r", "rooms": [13, 14, 15, 16, 17], }, "gardens": { "name": "Gardens Wing", "color": "|g", "rooms": [18, 19, 20, 21, 22], }, } # Limbo description LIMBO_DESC = """|wAcademy Central Hub|n You stand at the |ycrossroads of the Timmy Academy|n, a vast circular chamber where all wings converge. The domed ceiling overhead displays a |cshimmering map of the academy|n, with glowing pathways indicating the four great wings branching outward. Four |yornate archways|n mark the cardinal directions: |cNorth|n - The |cDormitory Wing|n (rest and residence) |yEast|n - The |yCommons Wing|n (gathering and fellowship) |rSouth|n - The |rWorkshop Wing|n (craft and creation) |gWest|n - The |gGardens Wing|n (nature and growth) A |ycrystal fountain|n at the center murmurs softly, its waters cycling through colors that reflect the mood of the academy. Carved into the marble floor is the academy motto: |x"Together we learn, together we grow."|n This is where all journeys begin and all paths return.""" LIMBO_ATMOSPHERE = { "mood": "welcoming, central, transitional, purposeful", "lighting": "soft crystal glow from the fountain, archway light", "sounds": "fountain murmuring, distant echoes from all wings", "smells": "clean stone, fountain mist, faint traces of all wings", "temperature": "perfectly comfortable, magically regulated", } LIMBO_OBJECTS = [ "crystal fountain cycling through colors", "ornate archways to four wings", "domed ceiling with shimmering academy map", "marble floor with carved motto", "welcoming bench near the fountain", ] def get_room(room_id): """Get a room object by ID.""" try: return ObjectDB.objects.get(id=room_id) except ObjectDB.DoesNotExist: print(f" [ERROR] Room #{room_id} not found!") return None def rebuild_rooms(): """Apply rich descriptions, typeclasses, and attributes to all rooms.""" print("\n[1/5] REBUILDING ROOMS - Applying typeclasses and descriptions") print("-" * 60) # Parse all wing source files print(" Parsing wing source files...") parsed_files = {} for filename in ["dormitory_entrance.py", "commons_wing.py", "workshop_wing.py", "gardens_wing.py"]: parsed_files[filename] = parse_wing_file(filename) class_count = len(parsed_files[filename]) print(f" {filename}: {class_count} classes found") # Handle Limbo specially limbo = get_room(2) if limbo: limbo.db.desc = LIMBO_DESC limbo.db.atmosphere = LIMBO_ATMOSPHERE limbo.db.objects = LIMBO_OBJECTS limbo.db.wing = "hub" limbo.db.wing_info = WING_INFO print(f"\n [OK] #{limbo.id} {limbo.db_key} - hub desc={len(LIMBO_DESC)}ch") # Apply attributes from parsed sources to each room for room_id, (tc_path, wing, src_file, cls_name) in sorted(ROOM_CONFIG.items()): room = get_room(room_id) if not room: continue old_tc = room.db_typeclass_path # Set typeclass path if old_tc != tc_path: room.db_typeclass_path = tc_path room.save() # Get parsed attributes for this class attrs = parsed_files.get(src_file, {}).get(cls_name, {}) if not attrs: print(f" [WARN] #{room_id} {room.db_key} - no parsed attrs for {cls_name}") continue # Apply description desc_len = 0 if "desc" in attrs: room.db.desc = attrs["desc"] desc_len = len(attrs["desc"]) # Apply atmosphere if "atmosphere" in attrs: room.db.atmosphere = attrs["atmosphere"] # Apply notable objects list if "objects" in attrs: room.db.objects = attrs["objects"] # Apply exit info (metadata, not actual exits) if "exits_info" in attrs: room.db.exits_info = attrs["exits_info"] # Apply features if present if "features" in attrs: room.db.features = attrs["features"] # Apply aliases if "aliases" in attrs: room.aliases.clear() room.aliases.add(attrs["aliases"]) # Set wing membership room.db.wing = wing tc_label = "tc CHANGED" if old_tc != tc_path else "tc ok" has_atmo = "atmo" if "atmosphere" in attrs else "no-atmo" print(f" [OK] #{room_id} {room.db_key} - {wing}, {tc_label}, desc={desc_len}ch, {has_atmo}") print(f"\n Total: {len(ROOM_CONFIG) + 1} rooms configured") def verify_exits(): """Check all exits are properly connected bidirectionally.""" print("\n[2/5] VERIFYING EXITS - Checking bidirectional connections") print("-" * 60) exits = ObjectDB.objects.filter(db_typeclass_path__icontains="exit") exit_pairs = {} for ex in exits: src = ex.db_location dst = ex.db_destination if src and dst: pair_key = (src.id, dst.id) exit_pairs[pair_key] = ex.db_key issues = [] for (src_id, dst_id), exit_key in sorted(exit_pairs.items()): reverse = (dst_id, src_id) if reverse not in exit_pairs: src_room = get_room(src_id) dst_room = get_room(dst_id) src_name = src_room.db_key if src_room else f"#{src_id}" dst_name = dst_room.db_key if dst_room else f"#{dst_id}" issues.append(f" [WARN] No return: '{exit_key}' {src_name} -> {dst_name}") if issues: for issue in issues: print(issue) print(f"\n {len(issues)} one-way exits (extra exits from fix_world.py - not harmful)") else: print(f" [OK] All {len(exits)} exits have bidirectional connections") wing_exits = {"hub": 0, "dormitory": 0, "commons": 0, "workshop": 0, "gardens": 0} for ex in exits: src = ex.db_location if src: for wing_key, info in WING_INFO.items(): if src.id in info["rooms"]: wing_exits[wing_key] += 1 break print(f"\n Exit distribution:") for wing_key, count in wing_exits.items(): print(f" {wing_key:12s}: {count} exits") print(f" {'total':12s}: {len(exits)} exits") def manage_characters(): """Move all characters to Limbo.""" print("\n[3/5] MANAGING CHARACTERS - Ensuring all are in Limbo") print("-" * 60) limbo = get_room(2) if not limbo: print(" [ERROR] Limbo not found!") return chars = ObjectDB.objects.filter(db_typeclass_path__icontains="character") for char in chars: loc = char.db_location if loc and loc.id == limbo.id: print(f" [OK] {char.db_key} already in Limbo") else: char.move_to(limbo, quiet=True) old_loc = loc.db_key if loc else "None" print(f" [MOVED] {char.db_key} from {old_loc} -> Limbo") print(f"\n Total: {chars.count()} characters managed") def setup_channels(): """Set up the Public channel if it doesn't exist.""" print("\n[4/5] SETTING UP CHANNELS") print("-" * 60) try: pub = ChannelDB.objects.filter(db_key="Public").first() if pub: print(f" [OK] Public channel already exists (ID={pub.id})") else: from evennia.utils.create import create_channel pub = create_channel( "Public", desc="General discussion channel for all academy members.", locks="control:perm(Admin);listen:all();send:all()", ) print(f" [CREATED] Public channel (ID={pub.id})") except Exception as e: print(f" [WARN] Channel setup: {e}") try: info = ChannelDB.objects.filter(db_key="MudInfo").first() if info: print(f" [OK] MudInfo channel already exists (ID={info.id})") except Exception: pass def print_summary(): """Print a summary of the world state.""" print("\n[5/5] WORLD SUMMARY") print("-" * 60) rooms = ObjectDB.objects.filter(id__gte=2, id__lte=22) exits = ObjectDB.objects.filter(db_typeclass_path__icontains="exit") chars = ObjectDB.objects.filter(db_typeclass_path__icontains="character") channels = ChannelDB.objects.all() print(f" Rooms: {rooms.count()}") print(f" Exits: {exits.count()}") print(f" Characters: {chars.count()}") print(f" Channels: {channels.count()}") print("\n Rooms by wing:") for wing_key, info in WING_INFO.items(): wing_rooms = [get_room(rid) for rid in info["rooms"]] wing_rooms = [r for r in wing_rooms if r] names = [r.db_key for r in wing_rooms] print(f" {info['name']} ({len(names)} rooms):") for name in names: print(f" - {name}") print("\n Description verification:") all_good = True for room_id in range(2, 23): room = get_room(room_id) if room: desc = room.attributes.get("desc", "") atmo = room.attributes.get("atmosphere", None) desc_len = len(desc) if desc else 0 has_atmo = "atmo" if atmo else "NO-ATMO" status = "OK" if desc_len > 200 else "SHORT" if status != "OK": all_good = False print(f" [{status:5s}] #{room_id} {room.db_key}: {desc_len}ch, {has_atmo}") print("\n Characters:") for char in chars: loc = char.db_location print(f" - {char.db_key} (in {loc.db_key if loc else 'nowhere'})") print("\n" + "=" * 60) if all_good: print(" REBUILD COMPLETE - ALL ROOMS HAVE RICH DESCRIPTIONS") else: print(" REBUILD COMPLETE") print("=" * 60) if __name__ == "__main__": print(BANNER) rebuild_rooms() verify_exits() manage_characters() setup_channels() print_summary()