import sys import os import asyncio from contextlib import asynccontextmanager from typing import List from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Query from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles import uvicorn # 确保可以导入 src 模块 sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) from src.sim.simulator import Simulator from src.classes.world import World from src.classes.calendar import Month, Year, create_month_stamp from src.run.create_map import create_cultivation_world_map, add_sect_headquarters from src.sim.new_avatar import make_avatars as _new_make from src.utils.config import CONFIG from src.classes.sect import sects_by_id from src.classes.color import serialize_hover_lines from src.classes.event import Event import random # 全局游戏实例 game_instance = { "world": None, "sim": None } class ConnectionManager: def __init__(self): self.active_connections: list[WebSocket] = [] async def connect(self, websocket: WebSocket): await websocket.accept() self.active_connections.append(websocket) def disconnect(self, websocket: WebSocket): self.active_connections.remove(websocket) async def broadcast(self, message: dict): import json try: # 简单序列化,实际生产可能需要更复杂的 Encoder txt = json.dumps(message, default=str) for connection in self.active_connections: await connection.send_text(txt) except Exception as e: print(f"Broadcast error: {e}") manager = ConnectionManager() def serialize_events_for_client(events: List[Event]) -> List[dict]: """将事件转换为前端可用的结构。""" serialized: List[dict] = [] for idx, event in enumerate(events): month_stamp = getattr(event, "month_stamp", None) stamp_int = None year = None month = None if month_stamp is not None: try: stamp_int = int(month_stamp) except Exception: stamp_int = None try: year = int(month_stamp.get_year()) except Exception: year = None try: month_obj = month_stamp.get_month() month = int(getattr(month_obj, "value", month_obj)) except Exception: month = None related_raw = getattr(event, "related_avatars", None) or [] related_ids = [str(a) for a in related_raw if a is not None] serialized.append({ "id": getattr(event, "event_id", None) or f"{stamp_int or 'evt'}-{idx}", "text": str(event), "content": getattr(event, "content", ""), "year": year, "month": month, "month_stamp": stamp_int, "related_avatar_ids": related_ids, "is_major": bool(getattr(event, "is_major", False)), "is_story": bool(getattr(event, "is_story", False)), }) return serialized def init_game(): """初始化游戏世界,逻辑复用自 src/run/run.py""" print("正在初始化游戏世界...") game_map = create_cultivation_world_map() world = World(map=game_map, month_stamp=create_month_stamp(Year(100), Month.JANUARY)) sim = Simulator(world) # 宗门初始化逻辑 all_sects = list(sects_by_id.values()) needed_sects = int(getattr(CONFIG.game, "sect_num", 0) or 0) # 简单的宗门抽样逻辑 (简化版) existed_sects = [] if needed_sects > 0 and all_sects: pool = list(all_sects) random.shuffle(pool) existed_sects = pool[:needed_sects] if existed_sects: add_sect_headquarters(world.map, existed_sects) # 创建角色 # 注意:这里直接调用 new_avatar 的 make_avatars,避免循环导入 all_avatars = _new_make(world, count=CONFIG.game.init_npc_num, current_month_stamp=world.month_stamp, existed_sects=existed_sects) world.avatar_manager.avatars.update(all_avatars) game_instance["world"] = world game_instance["sim"] = sim print("游戏世界初始化完成!") async def game_loop(): """后台自动运行游戏循环""" print("后台游戏循环已启动...") while True: # 控制游戏速度,例如每秒 1 次更新 await asyncio.sleep(1.0) try: sim = game_instance.get("sim") world = game_instance.get("world") if sim and world: # 执行一步 events = await sim.step() # 构造广播数据包 state = { "type": "tick", "year": int(world.month_stamp.get_year()), "month": world.month_stamp.get_month().value, "events": serialize_events_for_client(events), # 暂时只发前 50 个角色的位置更新,减少数据量 "avatars": [ { "id": str(a.id), "x": int(getattr(a, "pos_x", 0)), "y": int(getattr(a, "pos_y", 0)) } for a in list(world.avatar_manager.avatars.values())[:50] ] } await manager.broadcast(state) except Exception as e: print(f"Game loop error: {e}") @asynccontextmanager async def lifespan(app: FastAPI): # 启动时初始化 init_game() # 启动后台任务 asyncio.create_task(game_loop()) yield # 关闭时清理(如果需要) app = FastAPI(lifespan=lifespan) # 允许跨域,方便前端开发 app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 挂载静态资源 ASSETS_PATH = os.path.join(os.path.dirname(__file__), '..', '..', 'assets') if os.path.exists(ASSETS_PATH): app.mount("/assets", StaticFiles(directory=ASSETS_PATH), name="assets") else: print(f"Warning: Assets path not found: {ASSETS_PATH}") @app.get("/") def read_root(): return {"status": "online", "app": "Cultivation World Simulator Backend"} @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): await manager.connect(websocket) try: while True: # 保持连接活跃,接收客户端指令(目前暂不处理复杂指令) data = await websocket.receive_text() # echo test if data == "ping": await websocket.send_text('{"type":"pong"}') except WebSocketDisconnect: manager.disconnect(websocket) except Exception as e: print(f"WS Error: {e}") manager.disconnect(websocket) @app.get("/api/state") def get_state(): """获取当前世界的一个快照(调试模式)""" try: # 1. 基础检查 world = game_instance.get("world") if world is None: return {"step": 1, "error": "No world"} # 2. 时间检查 y = 0 m = 0 try: y = int(world.month_stamp.get_year()) m = int(world.month_stamp.get_month().value) except Exception as e: return {"step": 2, "error": str(e)} # 3. 角色列表检查 av_list = [] try: raw_avatars = list(world.avatar_manager.avatars.values())[:50] # 缩小范围 for a in raw_avatars: # 极其保守的取值 aid = str(getattr(a, "id", "no_id")) aname = str(getattr(a, "name", "no_name")) # 修正:使用 pos_x/pos_y ax = int(getattr(a, "pos_x", 0)) ay = int(getattr(a, "pos_y", 0)) aaction = "unknown" # 动作检查 curr = getattr(a, "current_action", None) if curr: act = getattr(curr, "action", None) if act: aaction = getattr(act, "name", "unnamed_action") else: aaction = str(curr) av_list.append({ "id": aid, "name": aname, "x": ax, "y": ay, "action": str(aaction), "gender": str(a.gender.value), "pic_id": (hash(a.id) % 15) + 1 }) except Exception as e: return {"step": 3, "error": str(e)} recent_events = [] try: event_manager = getattr(world, "event_manager", None) if event_manager: recent_events = serialize_events_for_client(event_manager.get_recent_events(limit=50)) except Exception: recent_events = [] return { "status": "ok", "year": y, "month": m, "avatar_count": len(world.avatar_manager.avatars), "avatars": av_list, "events": recent_events } except Exception as e: return {"step": 0, "error": "Fatal: " + str(e)} @app.get("/api/map") def get_map(): """获取静态地图数据(仅需加载一次)""" world = game_instance.get("world") if not world or not world.map: return {"error": "No map"} # 构造二维数组 w, h = world.map.width, world.map.height map_data = [] for y in range(h): row = [] for x in range(w): tile = world.map.get_tile(x, y) row.append(tile.type.name) map_data.append(row) # 构造区域列表 regions_data = [] if world.map and hasattr(world.map, 'regions'): for r in world.map.regions.values(): # 确保有中心点 if hasattr(r, 'center_loc') and r.center_loc: rtype = "unknown" if hasattr(r, 'get_region_type'): rtype = r.get_region_type() region_dict = { "id": r.id, "name": r.name, "type": rtype, "x": r.center_loc[0], "y": r.center_loc[1] } # 如果是宗门区域,额外传递 sect_name 用于前端加载图片 if hasattr(r, 'sect_name'): region_dict["sect_name"] = r.sect_name regions_data.append(region_dict) return { "width": w, "height": h, "data": map_data, "regions": regions_data } @app.post("/api/step") async def step_world(): """手动触发一帧(一个月)""" sim = game_instance["sim"] if not sim: return {"error": "Sim not initialized"} events = await sim.step() return { "message": "Step executed", "event_count": len(events), "events_sample": [str(e) for e in events[:5]] } @app.get("/api/hover") def get_hover_info( target_type: str = Query(alias="type"), target_id: str = Query(alias="id") ): world = game_instance.get("world") if world is None: raise HTTPException(status_code=503, detail="World not initialized") target = None if target_type == "avatar": target = world.avatar_manager.avatars.get(target_id) elif target_type == "region": if world.map and hasattr(world.map, "regions"): regions = world.map.regions target = regions.get(target_id) if target is None: try: target = regions.get(int(target_id)) except (ValueError, TypeError): target = None else: raise HTTPException(status_code=400, detail="Unsupported target type") if target is None: raise HTTPException(status_code=404, detail="Target not found") if not hasattr(target, "get_hover_info"): raise HTTPException(status_code=422, detail="Target has no hover info") lines = target.get_hover_info() or [] return { "id": target_id, "type": target_type, "name": getattr(target, "name", target_id), "lines": serialize_hover_lines([str(line) for line in lines]), } def start(): """启动服务的入口函数""" # 改为 8002 端口 uvicorn.run("src.server.main:app", host="0.0.0.0", port=8002, reload=False) if __name__ == "__main__": start()