scense_runtime/runtime/runtime_service.py
2026-08-29 21:10:24 +08:00

199 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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
绝不向上抛 500W-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'))}