# -*- 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'])