199 lines
7.4 KiB
Python
199 lines
7.4 KiB
Python
# -*- 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'))}
|