# -*- coding: utf-8 -*- """runtime 运行时执行引擎 — 服务端辅助层(W-10a~W-10i)。 职责边界 -------- * 运行时引擎核心是前端 `wwwroot/runtime.js`(事件循环 / 实体状态 / 脚本执行 / 事件分发 / 定时器 / 碰撞检测 / 暂停恢复 / 错误降级 / 独立世界本地引擎)。 * 本文件仅提供"可选服务端辅助":从世界模块读取世界与实体定义、构建运行态实体树、 提供状态/事件/控制接口;服务端不可用时前端自动降级独立模式(W-10i),不影响运行。 * 本模块为交互层模块,无自有数据表(无 models/json),只读复用 world 模块数据。 三处同步注册(漏一处 -> NameError,见 module-development-spec) -------------------------------------------------------------- ① 实现:本文件(runtime_service.py)定义全部 async 函数 ② 导出:runtime/__init__.py import 本文件函数 ③ 注册:runtime/init.py load_runtime(env) 中 env.xxx = xxx """ import time # 进程内运行时辅助状态(服务端辅助用;前端引擎状态以 runtime.js 为准) _RUNTIMES = {} def _now_str(): """统一时间字符串(不依赖 appPublic 工具,保持本文件零外部运行时依赖)。""" return time.strftime('%Y-%m-%d %H:%M:%S') def _degraded(world_id, reason): """W-10i 降级响应:后端不可用/查无数据 -> 前端切独立模式继续运行。""" return { 'ok': True, 'degraded': True, 'mode': 'independent', 'world': {'id': world_id, 'name': '独立世界', 'mode': 'independent'}, 'entities': [], 'message': 'backend degraded: ' + str(reason), } def _row_to_dict(row, fields): """DictObject -> dict(禁止 dict(row),见 module-development-spec)。""" d = {} for f in fields: v = getattr(row, f, None) if v is not None: d[f] = v return d def build_entity_tree(entities): """W-10a 构建运行态实体树:扁平实体列表 -> 父子树。 输入: entities = [{"id","name","type","parent_id","x","y","props",...}] 输出: 根节点数组 [{"id","name","type","state","x","y","props","children":[...]}] """ if not entities: return [] by_id = {} parent_map = {} for e in entities: eid = str(e.get('id') or e.get('entity_id') or '') if not eid: continue by_id[eid] = { 'id': eid, 'name': str(e.get('name') or eid), 'type': str(e.get('type') or 'object'), 'state': str(e.get('state') or 'idle'), 'x': e.get('x', 0), 'y': e.get('y', 0), 'props': e.get('props') or {}, 'children': [], } parent_map[eid] = e.get('parent_id') or e.get('parent') or None roots = [] for eid, node in by_id.items(): pid = parent_map.get(eid) if pid and pid in by_id: by_id[pid]['children'].append(node) else: roots.append(node) return roots async def load_world_runtime(world_id): """W-10a 世界运行时初始化(服务端辅助):读取世界 + 实体 -> 运行态实体树。 任何后端异常/查无数据 -> 返回 ok=True + degraded=True(前端降级独立模式 W-10i), 绝不向上抛 500(W-10h 错误处理与降级)。 """ if not world_id: return {'ok': False, 'message': 'world_id is required', 'data': None} try: from sqlor import DBPools from ahserver import ServerEnv except Exception as exc: return _degraded(world_id, 'backend import unavailable: ' + str(exc)) try: env = ServerEnv() dbname = env.get_module_dbname('world') except Exception as exc: return _degraded(world_id, 'world dbname unavailable: ' + str(exc)) if not dbname: return _degraded(world_id, 'world dbname empty') try: async with DBPools().sqlorContext(dbname) as sor: # 世界主档(world 模块表,列名以 models/world.json 为准:id/name/mode) world = None try: rows = await sor.sqlExe('SELECT * FROM world WHERE id=%s LIMIT 1', [world_id]) for r in rows or []: world = _row_to_dict(r, ['id', 'name', 'mode']) except Exception: world = None if not world: return _degraded(world_id, 'world not found in db') # 实体列表:优先 world_entity 表,退而求其次 scene 表(只读、尽力而为) entities = [] for table in ('world_entity', 'scene'): try: sql = 'SELECT * FROM ' + table + ' WHERE world_id=%s' rows = await sor.sqlExe(sql, [world_id]) except Exception: continue for r in rows or []: entities.append({ 'id': str(getattr(r, 'id', '') or ''), 'name': str(getattr(r, 'name', '') or ''), 'type': str(getattr(r, 'type', 'object') or 'object'), 'parent_id': getattr(r, 'parent_id', None), 'x': getattr(r, 'x', 0), 'y': getattr(r, 'y', 0), 'props': {}, }) if entities: break return { 'ok': True, 'degraded': False, 'mode': 'server', 'world': world, 'entities': entities, } except Exception as exc: return _degraded(world_id, 'db error: ' + str(exc)) async def runtime_status(runtime_id): """W-10g 运行时状态查询(服务端辅助,进程内内存态)。""" rt = _RUNTIMES.get(runtime_id or 'local') or {} return { 'runtime_id': runtime_id or 'local', 'running': bool(rt.get('running')), 'paused': bool(rt.get('paused')), 'mode': rt.get('mode', 'independent'), 'entities': len(rt.get('entities') or []), 'events': len(rt.get('events') or []), 'errors': len(rt.get('errors') or []), 'updated_at': rt.get('updated_at', ''), } async def runtime_event_dispatch(event_type, entity_id, payload): """W-10d 事件分发(服务端辅助):事件入进程内队列,供前端消费。""" if not event_type: return {'ok': False, 'message': 'event_type is required'} rt = _RUNTIMES.setdefault('local', {'events': [], 'errors': []}) rt['events'] = rt.get('events') or [] rt['events'].append({ 'type': event_type, 'entity_id': entity_id or '', 'payload': payload or {}, 'ts': _now_str(), }) return {'ok': True, 'queued': len(rt['events']), 'event_type': event_type} async def runtime_control(action): """W-10g 暂停/恢复/启动(服务端辅助)。""" rt = _RUNTIMES.setdefault('local', {'events': [], 'errors': []}) if action == 'start': rt['running'] = True rt['paused'] = False elif action == 'pause': rt['paused'] = True elif action == 'resume': rt['paused'] = False rt['running'] = True else: return {'ok': False, 'message': 'invalid action: ' + str(action)} rt['updated_at'] = _now_str() return {'ok': True, 'action': action, 'running': bool(rt.get('running')), 'paused': bool(rt.get('paused'))}