diff --git a/scense_game/init.py b/scense_game/init.py index 4a12092..b25e832 100644 --- a/scense_game/init.py +++ b/scense_game/init.py @@ -3,6 +3,9 @@ from scense_game.aggregation import ( game_create, game_workbench, scene_add, entity_add, ) +from scense_game.workbench_api import ( + workbench_tree, workbench_load_script, workbench_save_script, +) from scense_game.core import ( game_list_games, game_list_sessions, game_list_results, game_start_session, game_pause_session, game_resume_session, @@ -21,6 +24,9 @@ def load_scense_game(env=None): env.game_workbench = game_workbench env.scene_add = scene_add env.entity_add = entity_add + env.workbench_tree = workbench_tree + env.workbench_load_script = workbench_load_script + env.workbench_save_script = workbench_save_script env.game_list_games = game_list_games env.game_list_sessions = game_list_sessions env.game_list_results = game_list_results diff --git a/scense_game/workbench_api.py b/scense_game/workbench_api.py new file mode 100644 index 0000000..1fb9315 --- /dev/null +++ b/scense_game/workbench_api.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +"""域模型 v2 聚合 API(补):对象树 + 保存脚本(声明式工作台后端)。""" +import json + +from appPublic.log import info, error, exception +from sqlor.dbpools import DBPools + + +def _get_dbname(): + from ahserver.serverenv import ServerEnv + return ServerEnv().get_module_dbname('scense_game') + + +async def workbench_tree(ns): + """对象树(Tree 懒加载)。id 空=根(全部游戏+其世界); + id=game:xx→世界;world:xx→场景+世界级实体;scene:xx→实体。""" + gid = (ns or {}).get('game_id') or '' + nid = (ns or {}).get('id') or '' + nodes = [] + dbname = _get_dbname() + try: + async with DBPools().sqlorContext(dbname) as sor: + if not nid: + rows = await sor.sqlExe( + 'select id, name from game order by created_at', {}) + for r in rows: + nodes.append({'id': 'game:' + r.id, 'parentid': '', + 'text': '🎮 ' + r.name, 'is_leaf': 0, + 'obj_type': 'game', 'real_id': r.id}) + if gid: + w = await sor.sqlExe( + 'select id, name from world where game_id=${gid}$', {'gid': gid}) + for r in w: + nodes.append({'id': 'world:' + r.id, 'parentid': '', + 'text': '🌍 ' + r.name, 'is_leaf': 0, + 'obj_type': 'world', 'real_id': r.id, 'game_id': gid}) + elif nid.startswith('game:'): + g_real = nid.split(':', 1)[1] + w = await sor.sqlExe( + 'select id, name from world where game_id=${gid}$', {'gid': g_real}) + for r in w: + nodes.append({'id': 'world:' + r.id, 'parentid': nid, + 'text': '🌍 ' + r.name, 'is_leaf': 0, + 'obj_type': 'world', 'real_id': r.id, 'game_id': g_real}) + elif nid.startswith('world:'): + w_real = nid.split(':', 1)[1] + sc = await sor.sqlExe( + 'select id, name from scene where world_id=${wid}$', {'wid': w_real}) + for r in sc: + nodes.append({'id': 'scene:' + r.id, 'parentid': nid, + 'text': '🎬 ' + r.name, 'is_leaf': 0, + 'obj_type': 'scene', 'real_id': r.id}) + ent = await sor.sqlExe( + "select id, name from entity where world_id=${wid}$ and (scene_id is null or scene_id='')", + {'wid': w_real}) + for r in ent: + nodes.append({'id': 'entity:' + r.id, 'parentid': nid, + 'text': '📦 ' + r.name + '(世界级)', 'is_leaf': 1, + 'obj_type': 'entity', 'real_id': r.id}) + elif nid.startswith('scene:'): + s_real = nid.split(':', 1)[1] + ent = await sor.sqlExe( + 'select id, name from entity where scene_id=${sid}$', {'sid': s_real}) + for r in ent: + nodes.append({'id': 'entity:' + r.id, 'parentid': nid, + 'text': '📦 ' + r.name, 'is_leaf': 1, + 'obj_type': 'entity', 'real_id': r.id}) + return {'success': True, 'data': nodes} + except Exception as e: + exception('workbench_tree: %s' % e) + return {'success': False, 'message': '对象树查询失败: %s' % e, 'data': []} + + +async def workbench_load_script(ns): + """读当前对象+事件的已有脚本。ns: {obj_type, obj_id, game_id, trigger_event}""" + obj_type = (ns or {}).get('obj_type') or '' + obj_id = (ns or {}).get('obj_id') or '' + evt = (ns or {}).get('trigger_event') or '' + if not obj_id or not evt: + return {'success': False, 'message': 'obj_id/trigger_event 必填'} + dbname = _get_dbname() + try: + async with DBPools().sqlorContext(dbname) as sor: + col = {'game': 'game_id', 'world': 'world_id', + 'scene': 'scene_id', 'entity': 'entity_id'}.get(obj_type) + if not col: + return {'success': False, 'message': 'obj_type 非法'} + rows = await sor.sqlExe( + 'select id, name, content, world_id, entity_id from script_engine ' + 'where ' + col + '=${oid}$ and trigger_event=${evt}$ limit 1', + {'oid': obj_id, 'evt': evt}) + if rows: + return {'success': True, 'data': {'id': rows[0].id, 'name': rows[0].name, + 'content': rows[0].content or ''}} + return {'success': True, 'data': None} + except Exception as e: + exception('workbench_load_script: %s' % e) + return {'success': False, 'message': '读取脚本失败: %s' % e} + + +async def workbench_save_script(ns): + """保存脚本(存在则更新)。ns: {obj_type, obj_id, game_id, trigger_event, content}""" + from appPublic.uniqueID import getID + obj_type = (ns or {}).get('obj_type') or '' + obj_id = (ns or {}).get('obj_id') or '' + gid = (ns or {}).get('game_id') or '' + evt = (ns or {}).get('trigger_event') or '' + content = (ns or {}).get('content') or '' + if not obj_id or not evt: + return {'success': False, 'message': 'obj_id/trigger_event 必填'} + dbname = _get_dbname() + try: + async with DBPools().sqlorContext(dbname) as sor: + # 解析对象所属世界(world_id 为 NOT NULL 约束) + wid = '' + if obj_type == 'world': + wid = obj_id + elif obj_type == 'scene': + rows = await sor.sqlExe( + 'select world_id from scene where id=${oid}$', {'oid': obj_id}) + wid = rows[0].world_id if rows else '' + elif obj_type == 'entity': + rows = await sor.sqlExe( + 'select world_id from entity where id=${oid}$', {'oid': obj_id}) + wid = rows[0].world_id if rows else '' + elif obj_type == 'game': + rows = await sor.sqlExe( + 'select id from world where game_id=${gid}$', {'gid': obj_id}) + wid = rows[0].id if rows else '' + if not gid and wid: + rows = await sor.sqlExe( + 'select game_id from world where id=${wid}$', {'wid': wid}) + gid = (rows[0].game_id if rows and rows[0].game_id else '') or gid + if not wid: + return {'success': False, 'message': '无法解析对象所属世界'} + col = {'game': 'game_id', 'world': 'world_id', + 'scene': 'scene_id', 'entity': 'entity_id'}[obj_type] + old = await sor.sqlExe( + 'select id from script_engine where ' + col + '=${oid}$ and trigger_event=${evt}$ limit 1', + {'oid': obj_id, 'evt': evt}) + import time + now = time.strftime('%Y-%m-%d %H:%M:%S') + if old: + await sor.sqlExe( + 'update script_engine set content=${content}$, updated_at=${now}$ where id=${sid}$', + {'content': content, 'now': now, 'sid': old[0].id}) + sid = old[0].id + else: + sid = getID() + fields = {'id': sid, 'name': obj_type + '_' + obj_id[:8] + '_' + evt, + 'code': 'SC_' + obj_id[:8] + '_' + evt, + 'script_type': '0', 'trigger_event': evt, 'content': content, + 'status': 'active', 'world_id': wid, 'game_id': gid, + 'created_at': now, 'updated_at': now} + if col != 'world_id': + fields[col] = obj_id + await sor.C('script_engine', fields) + return {'success': True, 'data': {'script_id': sid}} + except Exception as e: + exception('workbench_save_script: %s' % e) + return {'success': False, 'message': '保存脚本失败: %s' % e} diff --git a/wwwroot/workbench/api/load_script.dspy b/wwwroot/workbench/api/load_script.dspy new file mode 100644 index 0000000..86696bf --- /dev/null +++ b/wwwroot/workbench/api/load_script.dspy @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# scense_game/workbench/api/load_script.dspy —— 读当前对象+事件的脚本 +r = await workbench_load_script(dict(params_kw)) +return json.dumps(r, ensure_ascii=False, default=str) diff --git a/wwwroot/workbench/api/save_script.dspy b/wwwroot/workbench/api/save_script.dspy new file mode 100644 index 0000000..d3541fd --- /dev/null +++ b/wwwroot/workbench/api/save_script.dspy @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# scense_game/workbench/api/save_script.dspy —— 保存脚本 +r = await workbench_save_script(dict(params_kw)) +return json.dumps(r, ensure_ascii=False, default=str) diff --git a/wwwroot/workbench/api/tree.dspy b/wwwroot/workbench/api/tree.dspy new file mode 100644 index 0000000..907d628 --- /dev/null +++ b/wwwroot/workbench/api/tree.dspy @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# scense_game/workbench/api/tree.dspy —— 对象树懒加载(Tree dataurl) +r = await workbench_tree(dict(params_kw)) +if not r.get('success'): + return [] +return r.get('data') or [] diff --git a/wwwroot/workbench/workbench.html b/wwwroot/workbench/workbench.html deleted file mode 100644 index 5ae59c2..0000000 --- a/wwwroot/workbench/workbench.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - -元景 · 游戏工作台 - - - -
-

🎮 游戏工作台

- - - - - - - -
-
-
-

对象树(世界 1 · 场景 N · 实体 N)

-
加载中…
-
-
-
- 未选择对象 - - - -
- -
-
-
- - - diff --git a/wwwroot/workbench/workbench.js b/wwwroot/workbench/workbench.js deleted file mode 100644 index c48eff1..0000000 --- a/wwwroot/workbench/workbench.js +++ /dev/null @@ -1,196 +0,0 @@ -/* 元景游戏工作台 v2:game 聚合根单页编辑器(内联输入,无 prompt) */ -(function () { - function api(path, params) { - return fetch(path, { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(params || {}) - }).then(function (r) { return r.json(); }); - } - function base() { return '/scense_game'; } - - var state = { game: null, data: null, sel: null, games: [], cur_script: null, inline: null }; - - function msg(t, ok) { - var el = document.getElementById('msg'); - el.textContent = t; - el.className = ok ? 'ok' : 'err'; - } - - function newGame() { - state.game = null; state.data = null; state.sel = null; state.cur_script = null; - document.getElementById('game_name').value = ''; - document.getElementById('game_sel').value = ''; - renderTree(null); - document.getElementById('cur_obj').textContent = '未选择对象'; - document.getElementById('code').value = ''; - msg('填写名称后点保存游戏(将自动创建 1:1 世界)', true); - } - - function saveGame() { - var name = document.getElementById('game_name').value.trim(); - if (state.game) { msg('当前游戏: ' + state.game.name + '(' + state.game.id + ')', true); return; } - if (!name) { msg('先填游戏名称', false); return; } - api(base() + '/game/api/game_create.dspy', {name: name, - description: document.getElementById('game_shared').checked ? 'shared' : 'independent'}) - .then(function (r) { - if (r.success) { msg('游戏创建成功,世界已自动建立', true); reload(r.data.game_id); } - else msg(r.message || '创建失败', false); - }); - } - - function switchGame(gid) { - if (!gid) return; - state.game = null; state.sel = null; - reload(gid); - } - - function reload(forceGid) { - api(base() + '/game/api/game_list.dspy', {}).then(function (r) { - var games = (r.data && r.data.games) || r || []; - if (!Array.isArray(games)) games = []; - state.games = games; - var sel = document.getElementById('game_sel'); - sel.innerHTML = '' + - games.map(function (g) { - var gid = g.game_id || g.id; - return ''; - }).join(''); - var gid = forceGid || (state.game && state.game.id) || (games[0] && (games[0].game_id || games[0].id)); - if (!gid) { state.game = null; renderTree(null); return; } - sel.value = gid; - api(base() + '/game/api/game_workbench.dspy', {game_id: gid}).then(function (w) { - if (!w.success) { msg(w.message, false); return; } - state.game = w.data.game; state.data = w.data; - document.getElementById('game_name').value = (w.data.game && w.data.game.name) || ''; - renderTree(w.data); - if (state.sel) loadSelKeep(); else selectWorld(); - }); - }); - } - - function renderTree(d) { - var el = document.getElementById('tree_body'); - if (!d || !d.world) { el.innerHTML = '
尚无游戏,先填名称保存
'; return; } - var h = ''; - h += node('world', d.world.id, '🌍 ' + esc(d.world.name)); - (d.scenes || []).forEach(function (s) { - h += node('scene', s.id, '🎬 ' + esc(s.name), 12); - }); - h += addRow('scene', '+ 场景', 12); - (d.entities || []).forEach(function (e) { - h += node('entity', e.id, '📦 ' + esc(e.name) + (e.scene_id ? '' : '(世界级)'), 24); - }); - h += addRow('entity', '+ 实体', 24); - el.innerHTML = h; - } - function node(type, id, label, ml) { - var sel = state.sel && state.sel.id === id ? ' sel' : ''; - return '
' + label + '
'; - } - function addRow(type, label, ml) { - if (state.inline === type) { - return '
' + - '' + - '
' + - '
'; - } - return '
' + label + '
'; - } - function esc(s) { return String(s || '').replace(/