143 lines
5.8 KiB
Python
143 lines
5.8 KiB
Python
# -*- coding: utf-8 -*-
|
||
import json
|
||
import logging
|
||
import re
|
||
from odoo import http
|
||
from odoo.http import request, Response
|
||
|
||
_logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _json(data):
|
||
"""返回 UTF-8 JSON 响应(与 game_base 一致),并附带 CORS 头以支持跨域(8080→8069)。"""
|
||
headers = {
|
||
'Content-Type': 'application/json; charset=utf-8',
|
||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||
'Access-Control-Allow-Headers': 'Content-Type',
|
||
}
|
||
try:
|
||
origin = request.httprequest.headers.get('Origin')
|
||
if origin:
|
||
headers['Access-Control-Allow-Origin'] = origin
|
||
headers['Access-Control-Allow-Credentials'] = 'true'
|
||
else:
|
||
headers['Access-Control-Allow-Origin'] = '*'
|
||
except Exception:
|
||
headers['Access-Control-Allow-Origin'] = '*'
|
||
return Response(json.dumps(data, ensure_ascii=False), headers=headers)
|
||
|
||
|
||
def _post_body():
|
||
try:
|
||
raw = request.httprequest.get_data(as_text=True) or '{}'
|
||
return json.loads(raw)
|
||
except Exception:
|
||
return {}
|
||
|
||
|
||
class YtWorldApi(http.Controller):
|
||
"""前端沙盘记忆接口(JSON over HTTP)。auth=user:需已登录 Odoo。
|
||
前端统一带 ?db=game 且 credentials:'same-origin',故路由须为 type='http'
|
||
并以 _json 返回原始 JSON(与 game-api.js 的 fetch().json() 对齐)。"""
|
||
|
||
@http.route('/yt_world/api/world', type='http', auth='user',
|
||
methods=['GET', 'OPTIONS'], csrf=False)
|
||
def world(self, **kw):
|
||
"""取或建当前用户世界,导出 seed + 地块覆盖。"""
|
||
user = request.env.user
|
||
rec = request.env['yt.world'].sudo().get_or_create_for_user(user)
|
||
return _json(rec.export_state())
|
||
|
||
@http.route('/yt_world/api/tiles/save', type='http', auth='user',
|
||
methods=['POST', 'OPTIONS'], csrf=False)
|
||
def save_tiles(self, **kw):
|
||
"""增量覆盖保存地块。body: {"tiles": [...]}"""
|
||
user = request.env.user
|
||
world = request.env['yt.world'].sudo().search(
|
||
[('user_id', '=', user.id)], limit=1)
|
||
if not world:
|
||
return _json({'ok': False, 'error': 'no_world'})
|
||
body = _post_body()
|
||
tiles = body.get('tiles') or []
|
||
world.import_tiles(tiles)
|
||
return _json({'ok': True, 'saved': len(tiles)})
|
||
|
||
@http.route('/yt_world/api/settings', type='http', auth='user',
|
||
methods=['POST', 'OPTIONS'], csrf=False)
|
||
def save_settings(self, **kw):
|
||
"""保存相机/UI 偏好。body: {"settings": {...}}"""
|
||
user = request.env.user
|
||
world = request.env['yt.world'].sudo().search(
|
||
[('user_id', '=', user.id)], limit=1)
|
||
body = _post_body()
|
||
settings = body.get('settings')
|
||
if world and settings is not None:
|
||
world.settings = settings
|
||
return _json({'ok': True})
|
||
|
||
@http.route('/yt_world/api/world/rename', type='http', auth='user',
|
||
methods=['POST', 'OPTIONS'], csrf=False)
|
||
def rename_world(self, **kw):
|
||
"""修改当前用户世界名称。body: {"name": "..."}
|
||
校验:trim 后 1-24 字符,禁控制字符,空白正常允许。"""
|
||
user = request.env.user
|
||
world = request.env['yt.world'].sudo().search(
|
||
[('user_id', '=', user.id)], limit=1)
|
||
if not world:
|
||
return _json({'ok': False, 'error': 'no_world'})
|
||
body = _post_body()
|
||
name = (body.get('name') or '').strip()
|
||
if not name:
|
||
return _json({'ok': False, 'error': 'empty'})
|
||
if len(name) > 24:
|
||
return _json({'ok': False, 'error': 'too_long'})
|
||
if re.search(r'[\x00-\x1f\x7f]', name):
|
||
return _json({'ok': False, 'error': 'invalid_char'})
|
||
old = world.name
|
||
world.name = name
|
||
_logger.info('yt.world rename uid=%s %r -> %r', user.id, old, name)
|
||
return _json({'ok': True, 'name': name})
|
||
|
||
# ========== 图鉴 API ==========
|
||
|
||
def _records_json(self, model, fields):
|
||
"""将 model 的全部记录转为 [{field: value}, ...] 格式(Json 字段解析为对象)。"""
|
||
recs = request.env[model].sudo().search([], order='sequence, id')
|
||
out = []
|
||
for r in recs:
|
||
item = {}
|
||
for f in fields:
|
||
val = getattr(r, f)
|
||
if isinstance(val, str):
|
||
try:
|
||
val = json.loads(val)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
item[f] = val
|
||
out.append(item)
|
||
return _json(out)
|
||
|
||
@http.route('/yt_world/api/codex/resources', type='http', auth='public',
|
||
methods=['GET', 'OPTIONS'], csrf=False)
|
||
def codex_resources(self, **kw):
|
||
return self._records_json('yt.codex.resource',
|
||
['code','name','icon','category','quality','source','uses','recipe','description'])
|
||
|
||
@http.route('/yt_world/api/codex/creatures', type='http', auth='public',
|
||
methods=['GET', 'OPTIONS'], csrf=False)
|
||
def codex_creatures(self, **kw):
|
||
return self._records_json('yt.codex.creature',
|
||
['code','name','icon','habitat','tameable','diet','products','description'])
|
||
|
||
@http.route('/yt_world/api/codex/monsters', type='http', auth='public',
|
||
methods=['GET', 'OPTIONS'], csrf=False)
|
||
def codex_monsters(self, **kw):
|
||
return self._records_json('yt.codex.monster',
|
||
['code','name','icon','tier','level_min','level_max','habitat','drops','description'])
|
||
|
||
@http.route('/yt_world/api/codex/races', type='http', auth='public',
|
||
methods=['GET', 'OPTIONS'], csrf=False)
|
||
def codex_races(self, **kw):
|
||
return self._records_json('yt.codex.race',
|
||
['code','name','icon','layer','traits','description'])
|