服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
638 lines
29 KiB
Python
638 lines
29 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Native API (127.0.0.1:18111) client for the DNF90 GM tool.
|
||
|
||
本文件依据服务端源码 (go-server/internal/app/dnf90/gm_tool_admin.go、
|
||
gm_admin.go、gm_tool_compat.go 以及 internal/services/dnfbridge/*) 精确实现
|
||
全部 GM 端点,字段名/类型/校验规则均与源码一致。
|
||
|
||
两类端点:
|
||
- 读端点 GET /dnf/gm/... (返回 {"success":true,"data":...})
|
||
- 写端点 POST /dnf/gm/... 或 /local/dnf-gm/... (需要 X-Admin-Confirm)
|
||
|
||
认证头:
|
||
X-Admin-Token : adminToken(来自 instance.json)
|
||
X-Admin-Confirm: 写操作设为 "1"
|
||
"""
|
||
import json
|
||
import os
|
||
import uuid
|
||
import urllib.request
|
||
import urllib.error
|
||
from urllib.parse import urlencode
|
||
|
||
|
||
class NativeError(Exception):
|
||
pass
|
||
|
||
|
||
class NativeClient(object):
|
||
def __init__(self, base_url="http://127.0.0.1:18111", token=""):
|
||
self.base_url = base_url.rstrip("/")
|
||
self.token = token
|
||
|
||
# ---- low-level HTTP ------------------------------------------
|
||
def _request(self, method, path, body=None, confirm=False):
|
||
url = self.base_url + path
|
||
data = None
|
||
headers = {"Accept": "application/json"}
|
||
if self.token:
|
||
headers["X-Admin-Token"] = self.token
|
||
if confirm:
|
||
headers["X-Admin-Confirm"] = "1"
|
||
headers["X-Admin-Idempotency-Key"] = uuid.uuid4().hex
|
||
if body is not None:
|
||
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
||
headers["Content-Type"] = "application/json"
|
||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||
raw = resp.read()
|
||
if not raw:
|
||
return {}
|
||
return json.loads(raw.decode("utf-8"))
|
||
except urllib.error.HTTPError as e:
|
||
raw = e.read().decode("utf-8", "replace")
|
||
# 尝试提取服务端返回的 error 字段,给出更友好的提示
|
||
try:
|
||
parsed = json.loads(raw)
|
||
msg = parsed.get("error") or raw[:200]
|
||
except Exception:
|
||
msg = raw[:200]
|
||
raise NativeError("HTTP %d: %s" % (e.code, msg))
|
||
except urllib.error.URLError as e:
|
||
raise NativeError("连接失败: %s" % e.reason)
|
||
|
||
def _get(self, path):
|
||
return self._request("GET", path)
|
||
|
||
def _post(self, path, body=None, confirm=True):
|
||
return self._request("POST", path, body=body, confirm=confirm)
|
||
|
||
def _unwrap(self, raw):
|
||
"""取服务端统一响应里的 data 字段。"""
|
||
if isinstance(raw, dict) and "data" in raw:
|
||
return raw["data"]
|
||
return raw
|
||
|
||
# ---- 状态 / 账号 / 角色 --------------------------------------
|
||
def ping(self):
|
||
return self._get("/dnf/gm/status")
|
||
|
||
def list_accounts(self, query="", limit=50):
|
||
qs = urlencode({"q": query, "limit": limit})
|
||
try:
|
||
return self._unwrap(self._get("/dnf/gm/accounts?" + qs)) or []
|
||
except NativeError:
|
||
return []
|
||
|
||
def get_account(self, account_id):
|
||
return self._unwrap(self._get("/dnf/gm/accounts/%s" % account_id))
|
||
|
||
def set_account_value(self, account_id, field, value):
|
||
"""POST /dnf/gm/accounts/{account_id}/value {field, value:uint64}"""
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/accounts/%s/value" % account_id,
|
||
{"field": field, "value": int(value)}))
|
||
|
||
def list_characters(self, account_id=None):
|
||
"""角色按账号枚举:/dnf/gm/accounts/{id}/characters。
|
||
无 account_id 时遍历所有账号。"""
|
||
account_ids = []
|
||
if account_id:
|
||
account_ids = [account_id]
|
||
else:
|
||
accs = self.list_accounts()
|
||
account_ids = [a.get("account_id") for a in accs
|
||
if isinstance(a, dict) and a.get("account_id")]
|
||
out = []
|
||
for aid in account_ids:
|
||
try:
|
||
raw = self._unwrap(self._get("/dnf/gm/accounts/%s/characters" % aid))
|
||
except NativeError:
|
||
continue
|
||
if not isinstance(raw, list):
|
||
raw = [raw] if raw else []
|
||
for c in raw:
|
||
if not isinstance(c, dict):
|
||
continue
|
||
cid = c.get("character_id") or c.get("id")
|
||
out.append({
|
||
"character_id": str(cid),
|
||
"account_id": c.get("account_id", aid),
|
||
"name": c.get("name", ""),
|
||
"job": c.get("job", "0"),
|
||
"level": c.get("level", 0),
|
||
"gold": c.get("gold", 0),
|
||
"profession_name": c.get("profession_name", ""),
|
||
"grow_type": c.get("grow_type", 0),
|
||
"first_grow": c.get("first_grow", 0),
|
||
"awakening": c.get("awakening", 0),
|
||
"online": c.get("online", False),
|
||
"raw": c,
|
||
})
|
||
return out
|
||
|
||
def get_character(self, character_id):
|
||
raw = self._unwrap(self._get("/dnf/gm/characters/%s" % character_id))
|
||
if not isinstance(raw, dict):
|
||
return raw
|
||
stats = {k: v for k, v in raw.items()
|
||
if k not in ("character_id", "account_id", "name", "job",
|
||
"level", "slot", "profession_name", "grow_type",
|
||
"first_grow", "awakening", "online", "gold", "cera")}
|
||
if "gold" in raw:
|
||
stats["gold"] = raw["gold"]
|
||
out = {
|
||
"character_id": str(raw.get("character_id") or character_id),
|
||
"account_id": raw.get("account_id", ""),
|
||
"name": raw.get("name", ""),
|
||
"job": raw.get("job", "0"),
|
||
"level": raw.get("level", 0),
|
||
"gold": raw.get("gold", 0),
|
||
"profession_name": raw.get("profession_name", ""),
|
||
"grow_type": raw.get("grow_type", 0),
|
||
"first_grow": raw.get("first_grow", 0),
|
||
"awakening": raw.get("awakening", 0),
|
||
"online": raw.get("online", False),
|
||
"stats": stats,
|
||
}
|
||
return out
|
||
|
||
def get_character_detail(self, character_id):
|
||
return self._unwrap(self._get("/dnf/gm/characters/%s/detail" % character_id))
|
||
|
||
def get_inventory(self, character_id):
|
||
return self._unwrap(self._get("/dnf/gm/characters/%s/inventory" % character_id))
|
||
|
||
def get_equipment(self, character_id):
|
||
return self._unwrap(self._get("/dnf/gm/characters/%s/equipment-slots" % character_id))
|
||
|
||
def get_mailbox(self, character_id):
|
||
return self._unwrap(self._get("/dnf/gm/characters/%s/mailbox" % character_id))
|
||
|
||
def get_account_cubes(self, character_id):
|
||
return self._unwrap(self._get("/dnf/gm/characters/%s/account-cubes" % character_id))
|
||
|
||
# ---- 物品目录 / 搜索 -----------------------------------------
|
||
def get_item_detail(self, item_id):
|
||
return self._unwrap(self._get("/dnf/gm/items/%s/detail" % item_id))
|
||
|
||
def get_item_mail_options(self, item_id, mode="", query="", rarity=-1, limit=40, offset=0):
|
||
qs = urlencode({"mode": mode, "q": query, "rarity": rarity,
|
||
"limit": limit, "offset": offset})
|
||
return self._unwrap(self._get("/dnf/gm/items/%s/mail-options?%s" % (item_id, qs)))
|
||
|
||
def get_item_categories(self):
|
||
return self._unwrap(self._get("/dnf/gm/items/categories"))
|
||
|
||
def search_items(self, query="", kind="", tag="", segment="", group="", job="",
|
||
min_level=0, max_level=0, rarity=-1, expiration="",
|
||
offset=0, limit=50):
|
||
"""GET /dnf/gm/items/page —— 完整筛选参数(与源码 GMItemQuery 一致)。
|
||
返回归一化结构 {items:[...], total, limit, offset}。"""
|
||
params = {
|
||
"q": query, "kind": kind, "tag": tag, "segment": segment,
|
||
"group": group, "job": job, "min_level": min_level,
|
||
"max_level": max_level, "rarity": rarity, "expiration": expiration,
|
||
"offset": offset, "limit": limit,
|
||
}
|
||
# 去掉空值参数,避免干扰
|
||
params = {k: v for k, v in params.items() if v not in ("", -1, 0) or k in ("offset", "limit", "q", "rarity")}
|
||
qs = urlencode(params)
|
||
raw = self._unwrap(self._get("/dnf/gm/items/page?" + qs))
|
||
if isinstance(raw, dict) and "items" in raw:
|
||
return raw
|
||
return {"items": raw if isinstance(raw, list) else [], "total": 0,
|
||
"limit": limit, "offset": offset}
|
||
|
||
def search_avatar_sets(self, query="", job="", limit=24, offset=0):
|
||
qs = urlencode({"q": query, "job": job, "limit": limit, "offset": offset})
|
||
return self._unwrap(self._get("/dnf/gm/avatar-sets?" + qs))
|
||
|
||
# ---- 物品发放 -------------------------------------------------
|
||
# 注意:服务端有两个发放端点:
|
||
# 1) POST /local/dnf-gm/item/grant —— 直接进背包(服务端决定格位)
|
||
# 请求体 {character_id:string, item_id:uint32, count:uint32}
|
||
# 2) POST /local/dnf-gm/item/grant-options —— 带强化/增幅/锻造/品质/耐久/附魔/徽章
|
||
# 3) POST /local/dnf-gm/item/grant-with-emblems —— 带附魔宝珠 + 徽章(装备/时装)
|
||
# 4) POST /dnf/gm/characters/{id}/mail —— 邮件发放(带完整选项)
|
||
|
||
def grant_item(self, character_id, item_id, count=1):
|
||
body = {"character_id": str(character_id),
|
||
"item_id": int(item_id),
|
||
"count": int(count)}
|
||
return self._unwrap(self._post("/local/dnf-gm/item/grant", body))
|
||
|
||
def grant_item_options(self, character_id, item_id, count=1, reinforce=0,
|
||
amplify_type=0, forging=0, equipment_quality="",
|
||
durability=None, expire_days=0, emblem_item_ids=None,
|
||
enchant_bead_item_id=0):
|
||
"""带强化/增幅/锻造/品质/耐久/附魔的发放。
|
||
校验范围(源码 gm_tool_items_extra.go):
|
||
reinforce 0..31, amplify_type 0..4, forging 0..8, expire_days 0..3650"""
|
||
body = {"character_id": str(character_id), "item_id": int(item_id),
|
||
"count": int(count)}
|
||
if reinforce:
|
||
body["reinforce"] = int(reinforce)
|
||
if amplify_type:
|
||
body["amplify_type"] = int(amplify_type)
|
||
if forging:
|
||
body["forging"] = int(forging)
|
||
if equipment_quality:
|
||
body["equipment_quality"] = equipment_quality
|
||
if durability is not None:
|
||
body["durability"] = int(durability)
|
||
if expire_days:
|
||
body["expire_days"] = int(expire_days)
|
||
if emblem_item_ids:
|
||
body["emblem_item_ids"] = [int(x) for x in emblem_item_ids]
|
||
if enchant_bead_item_id:
|
||
body["enchant_bead_item_id"] = int(enchant_bead_item_id)
|
||
return self._unwrap(self._post("/local/dnf-gm/item/grant-options", body))
|
||
|
||
def grant_item_with_emblems(self, character_id, item_id, enchant_bead_item_id=0,
|
||
enchant_upgrade_count=None, emblems=None, open_sockets=None):
|
||
"""带附魔宝珠 + 徽章发放(仅装备/时装)。
|
||
emblems: [{socket_index:int, emblem_item_id:int, emblem_slot:int}, ...]"""
|
||
body = {"character_id": str(character_id), "item_id": int(item_id)}
|
||
if enchant_bead_item_id:
|
||
body["enchant_bead_item_id"] = int(enchant_bead_item_id)
|
||
if enchant_upgrade_count is not None:
|
||
body["enchant_upgrade_count"] = int(enchant_upgrade_count)
|
||
if emblems:
|
||
body["emblems"] = emblems
|
||
if open_sockets is not None:
|
||
body["open_sockets"] = int(open_sockets)
|
||
return self._unwrap(self._post("/local/dnf-gm/item/grant-with-emblems", body))
|
||
|
||
def send_item_mail(self, character_id, item_id, count=1, title="", body="",
|
||
expire_days=0, reinforce=0, amplify_type=0,
|
||
equipment_quality="", forging=0, emblem_item_ids=None,
|
||
enchant_bead_item_id=0):
|
||
"""POST /dnf/gm/characters/{id}/mail —— 邮件发放(完整选项)。"""
|
||
req = {"item_id": int(item_id), "count": int(count)}
|
||
if title:
|
||
req["title"] = title
|
||
if body:
|
||
req["body"] = body
|
||
if expire_days:
|
||
req["expire_days"] = int(expire_days)
|
||
if reinforce:
|
||
req["reinforce"] = int(reinforce)
|
||
if amplify_type:
|
||
req["amplify_type"] = int(amplify_type)
|
||
if equipment_quality:
|
||
req["equipment_quality"] = equipment_quality
|
||
if forging:
|
||
req["forging"] = int(forging)
|
||
if emblem_item_ids:
|
||
req["emblem_item_ids"] = [int(x) for x in emblem_item_ids]
|
||
if enchant_bead_item_id:
|
||
req["enchant_bead_item_id"] = int(enchant_bead_item_id)
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/characters/%s/mail" % character_id, req))
|
||
|
||
def delete_item(self, character_id, collection="", list_type=None, slot=None,
|
||
entry_key=""):
|
||
"""POST /local/dnf-gm/item/delete —— 删除背包行或穿戴装备行。
|
||
collection: "slots"(背包,默认) 或 "wear"(穿戴装备)"""
|
||
req = {"character_id": str(character_id)}
|
||
if collection:
|
||
req["collection"] = collection
|
||
if entry_key:
|
||
req["entry_key"] = entry_key
|
||
else:
|
||
req["list_type"] = int(list_type)
|
||
req["slot"] = int(slot)
|
||
return self._unwrap(self._post("/local/dnf-gm/item/delete", req))
|
||
|
||
# ---- 属性修改 -------------------------------------------------
|
||
def set_gold(self, character_id, amount):
|
||
"""POST /dnf/gm/characters/{id}/gold {value:int64}"""
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/characters/%s/gold" % str(character_id), {"value": int(amount)}))
|
||
|
||
def set_level(self, character_id, level):
|
||
"""POST /dnf/gm/characters/{id}/level {level:int}"""
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/characters/%s/level" % str(character_id), {"level": int(level)}))
|
||
|
||
def set_skill_bonus(self, character_id, bonus_sp, bonus_tp):
|
||
"""POST /dnf/gm/characters/{id}/skill-points {bonus_sp, bonus_tp:int64}"""
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/characters/%s/skill-points" % str(character_id),
|
||
{"bonus_sp": int(bonus_sp), "bonus_tp": int(bonus_tp)}))
|
||
|
||
def get_career(self, character_id):
|
||
return self._unwrap(self._get("/dnf/gm/characters/%s/career" % character_id))
|
||
|
||
def set_career(self, character_id, job, first, awakening):
|
||
"""POST /dnf/gm/characters/{id}/career {job:int, first:int, awakening:int}
|
||
这是修改职业/转职的正确接口(旧 /local/dnf-gm/character/job 语义不同)。"""
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/characters/%s/career" % str(character_id),
|
||
{"job": int(job), "first": int(first), "awakening": int(awakening)}))
|
||
|
||
def set_cera(self, account_id, amount=None, value=None, currency=""):
|
||
"""POST /local/dnf-gm/account/cera
|
||
{account_id, currency, amount(增量) 或 value(覆写)}
|
||
currency: cera / token_cera / happy_token_cera,默认 cera"""
|
||
req = {"account_id": str(account_id)}
|
||
if currency:
|
||
req["currency"] = currency
|
||
if amount is not None:
|
||
req["amount"] = int(amount)
|
||
elif value is not None:
|
||
req["value"] = int(value)
|
||
return self._unwrap(self._post("/local/dnf-gm/account/cera", req))
|
||
|
||
def adjust_revive_coin(self, character_id, amount=None, value=None, source=""):
|
||
"""POST /local/dnf-gm/character/revive-coin
|
||
amount(增量) 或 value(覆写),二者互斥"""
|
||
req = {"character_id": str(character_id)}
|
||
if amount is not None:
|
||
req["amount"] = int(amount)
|
||
elif value is not None:
|
||
req["value"] = int(value)
|
||
if source:
|
||
req["source"] = source
|
||
return self._unwrap(self._post("/local/dnf-gm/character/revive-coin", req))
|
||
|
||
def query_wallet(self, character_id="", character_name="", currency=""):
|
||
"""POST /local/dnf-gm/wallet/query"""
|
||
req = {}
|
||
if character_id:
|
||
req["character_id"] = str(character_id)
|
||
if character_name:
|
||
req["character_name"] = character_name
|
||
if currency:
|
||
req["currency"] = currency
|
||
return self._unwrap(self._post("/local/dnf-gm/wallet/query", req))
|
||
|
||
def update_wallet(self, character_id="", character_name="", currency="",
|
||
mode="", amount=0):
|
||
"""POST /local/dnf-gm/wallet/update
|
||
currency + mode + amount;mode 语义见源码 gm_admin_state.go"""
|
||
req = {}
|
||
if character_id:
|
||
req["character_id"] = str(character_id)
|
||
if character_name:
|
||
req["character_name"] = character_name
|
||
if currency:
|
||
req["currency"] = currency
|
||
if mode:
|
||
req["mode"] = mode
|
||
if amount:
|
||
req["amount"] = int(amount)
|
||
return self._unwrap(self._post("/local/dnf-gm/wallet/update", req))
|
||
|
||
# ---- 背包 / 装备 ---------------------------------------------
|
||
def inventory_list(self, character_id):
|
||
"""GET /local/dnf-gm/inventory/list —— 列角色背包全部行 + 穿戴装备行。
|
||
返回 {"character_id":..., "rows":[{collection,entry_key,item_id,count,bind,item_name},...]}"""
|
||
return self._unwrap(self._get(
|
||
"/local/dnf-gm/inventory/list?character_id=%s" % character_id))
|
||
|
||
def delete_inventory_item(self, character_id, container, key, count):
|
||
"""POST /dnf/gm/characters/{id}/inventory/delete {container, key, count}"""
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/characters/%s/inventory/delete" % str(character_id),
|
||
{"container": container, "key": key, "count": int(count)}))
|
||
|
||
def clear_inventory(self, character_id, container="", keep_wallet=True, collection=""):
|
||
"""POST /local/dnf-gm/inventory/clear {character_id, collection, keep_wallet}
|
||
工具端版本会保留主背包 0:0~0:2 的固定钱包槽(金币/复活币/点券载体)。"""
|
||
req = {"character_id": str(character_id)}
|
||
if collection:
|
||
req["collection"] = collection
|
||
req["keep_wallet"] = bool(keep_wallet)
|
||
return self._unwrap(self._post("/local/dnf-gm/inventory/clear", req))
|
||
|
||
def unequip_equipment(self, character_id, key):
|
||
"""POST /dnf/gm/characters/{id}/equipment/unequip {key}"""
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/characters/%s/equipment/unequip" % str(character_id), {"key": key}))
|
||
|
||
def delete_equipment(self, character_id, key):
|
||
"""POST /dnf/gm/characters/{id}/equipment/delete {key}"""
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/characters/%s/equipment/delete" % str(character_id), {"key": key}))
|
||
|
||
def unlock_equip_slots(self, character_id, slots):
|
||
"""POST /dnf/gm/characters/{id}/equipment-slots/unlock {slots:[]string}"""
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/characters/%s/equipment-slots/unlock" % str(character_id),
|
||
{"slots": slots}))
|
||
|
||
def set_account_cube(self, character_id, selection, count):
|
||
"""POST /dnf/gm/characters/{id}/account-cubes {selection:int, count:int64}"""
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/characters/%s/account-cubes" % str(character_id),
|
||
{"selection": int(selection), "count": int(count)}))
|
||
|
||
# ---- 邮件 -----------------------------------------------------
|
||
def send_system_mail(self, character_id="", character_name="", title="",
|
||
body="", gold=0, attachments=None):
|
||
"""POST /local/dnf-gm/mail {character_id/name, title, body, gold, attachments}
|
||
attachments: [{item_id, count, bind, reinforce_level, amplify_level, amplify_type}]"""
|
||
req = {}
|
||
if character_id:
|
||
req["character_id"] = str(character_id)
|
||
if character_name:
|
||
req["character_name"] = character_name
|
||
if title:
|
||
req["title"] = title
|
||
if body:
|
||
req["body"] = body
|
||
if gold:
|
||
req["gold"] = int(gold)
|
||
if attachments:
|
||
req["attachments"] = attachments
|
||
return self._unwrap(self._post("/local/dnf-gm/mail", req))
|
||
|
||
def clear_mailbox(self, character_id="", character_name="", confirm=True):
|
||
"""POST /local/dnf-gm/mail/clear {character_id/name, confirm}"""
|
||
req = {"confirm": bool(confirm)}
|
||
if character_id:
|
||
req["character_id"] = str(character_id)
|
||
if character_name:
|
||
req["character_name"] = character_name
|
||
return self._unwrap(self._post("/local/dnf-gm/mail/clear", req))
|
||
|
||
def delete_mailbox_mails(self, character_id, mail_ids, force=False):
|
||
"""POST /dnf/gm/characters/{id}/mailbox/delete {mail_ids, force}"""
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/characters/%s/mailbox/delete" % str(character_id),
|
||
{"mail_ids": mail_ids, "force": bool(force)}))
|
||
|
||
# ---- 拍卖行(服务端 GM 接口,线上上架/下架)------------------
|
||
def auction_listings(self):
|
||
"""GET /local/dnf-gm/auction/listings —— 列出所有挂单(含过期)。
|
||
返回 {"listings":[...], "escrow_gold":int}。"""
|
||
return self._unwrap(self._get("/local/dnf-gm/auction/listings"))
|
||
|
||
def auction_list(self, seller_id, item_id, count, unit_price,
|
||
start_price=None, buyout_price=None, duration_hours=24,
|
||
grant_if_missing=False):
|
||
"""POST /local/dnf-gm/auction/list —— 上架。
|
||
服务端 GMAuctionListRequest 字段:
|
||
seller_id / seller_name / item_id / count / unit_price /
|
||
start_price(每件起拍,-1=不设) / buyout_price(每件一口价,-1=不设) /
|
||
duration_hours(24 或 48) / grant_if_missing(背包不足自动发放差额)"""
|
||
req = {
|
||
"seller_id": str(seller_id),
|
||
"item_id": int(item_id),
|
||
"count": int(count),
|
||
"unit_price": int(unit_price),
|
||
}
|
||
# start/buyout:None 或空 -> -1(服务端约定 -1 表示不设)
|
||
req["start_price"] = int(start_price) if start_price not in (None, "") else -1
|
||
req["buyout_price"] = int(buyout_price) if buyout_price not in (None, "") else -1
|
||
req["duration_hours"] = int(duration_hours) if duration_hours else 24
|
||
if grant_if_missing:
|
||
req["grant_if_missing"] = True
|
||
return self._unwrap(self._post("/local/dnf-gm/auction/list", req))
|
||
|
||
def auction_cancel(self, listing_id):
|
||
"""POST /local/dnf-gm/auction/cancel —— 下架。
|
||
GMAuctionCancelRequest: {listing_id:uint64}。"""
|
||
return self._unwrap(self._post(
|
||
"/local/dnf-gm/auction/cancel", {"listing_id": int(listing_id)}))
|
||
|
||
# ---- 任务 -----------------------------------------------------
|
||
def get_quest_overview(self, character_id):
|
||
return self._unwrap(self._get("/dnf/gm/characters/%s/quests" % character_id))
|
||
|
||
def search_quests(self, character_id, query, limit=40):
|
||
qs = urlencode({"q": query, "limit": limit})
|
||
return self._unwrap(self._get(
|
||
"/dnf/gm/characters/%s/quests/search?%s" % (character_id, qs)))
|
||
|
||
def get_quest_detail(self, character_id, quest_id):
|
||
return self._unwrap(self._get(
|
||
"/dnf/gm/characters/%s/quests/%s/detail" % (character_id, quest_id)))
|
||
|
||
def _quest_mutate(self, character_id, quest_id, action):
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/characters/%s/quests/%s/%s" % (character_id, quest_id, action), {}))
|
||
|
||
def accept_quest(self, character_id, quest_id):
|
||
return self._quest_mutate(character_id, quest_id, "accept")
|
||
|
||
def mark_quest_ready(self, character_id, quest_id):
|
||
return self._quest_mutate(character_id, quest_id, "ready")
|
||
|
||
def give_up_quest(self, character_id, quest_id):
|
||
return self._quest_mutate(character_id, quest_id, "give-up")
|
||
|
||
def complete_quest(self, character_id, quest_id):
|
||
return self._quest_mutate(character_id, quest_id, "complete")
|
||
|
||
def uncomplete_quest(self, character_id, quest_id):
|
||
return self._quest_mutate(character_id, quest_id, "uncomplete")
|
||
|
||
def complete_quest_batch(self, character_id, quest_ids):
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/characters/%s/quests/complete-batch" % character_id,
|
||
{"quest_ids": [int(x) for x in quest_ids]}))
|
||
|
||
# ---- 副本 -----------------------------------------------------
|
||
def get_dungeons(self, character_id):
|
||
return self._unwrap(self._get("/dnf/gm/characters/%s/dungeons" % character_id))
|
||
|
||
def unlock_dungeon_difficulties(self, character_id):
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/characters/%s/dungeons/unlock-difficulties" % character_id, {}))
|
||
|
||
def unlock_hell_challenges(self, character_id):
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/characters/%s/dungeons/unlock-hell" % character_id, {}))
|
||
|
||
# ---- 黑钻 -----------------------------------------------------
|
||
def get_black_diamond(self, character_id="", character_name=""):
|
||
qs = urlencode({"id": character_id, "name": character_name})
|
||
return self._unwrap(self._get("/local/dnf-gm/character/black-diamond?" + qs))
|
||
|
||
def activate_black_diamond(self, character_id, days=0):
|
||
req = {"character_id": str(character_id)}
|
||
if days:
|
||
req["days"] = int(days)
|
||
return self._unwrap(self._post("/local/dnf-gm/character/black-diamond", req))
|
||
|
||
# ---- 运营参数 / 爆率(Native,服务端在线) ----------------------
|
||
def get_gameplay_settings(self):
|
||
"""GET /dnf/gm/gameplay -> GMGameplaySettings(深渊史诗掉率等运营参数)"""
|
||
return self._unwrap(self._get("/dnf/gm/gameplay"))
|
||
|
||
def set_hell_drop_rate(self, percent):
|
||
"""POST /dnf/gm/gameplay/hell-drop-rate {percent:int}(深渊史诗掉落率百分比)"""
|
||
return self._unwrap(self._post("/dnf/gm/gameplay/hell-drop-rate",
|
||
{"percent": int(percent)}))
|
||
|
||
def set_epic_potion_multiplier(self, percent):
|
||
"""POST /dnf/gm/gameplay/epic-potion-test-multiplier {percent:int}
|
||
城镇增益药剂测试倍率,0 = 关闭按 PVF 表走。"""
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/gameplay/epic-potion-test-multiplier",
|
||
{"percent": int(percent)}))
|
||
|
||
def set_fatigue_mode(self, mode):
|
||
"""POST /dnf/gm/gameplay/fatigue-mode {mode:str}
|
||
mode: none(正常)/ free(无限不扣)/ lock(锁死高值)/ refill(一键回满)。"""
|
||
return self._unwrap(self._post("/dnf/gm/gameplay/fatigue-mode",
|
||
{"mode": str(mode)}))
|
||
|
||
def set_burning(self, enabled):
|
||
"""POST /dnf/gm/gameplay/burning {enabled:bool}
|
||
燃烧疲劳:双倍消耗 + 通关经验翻倍。"""
|
||
return self._unwrap(self._post("/dnf/gm/gameplay/burning",
|
||
{"enabled": bool(enabled)}))
|
||
|
||
def refill_fatigue(self, character_id):
|
||
"""POST /dnf/gm/characters/{id}/fatigue/refill 一键回满角色疲劳。"""
|
||
return self._unwrap(self._post(
|
||
"/dnf/gm/characters/%s/fatigue/refill" % str(character_id), {}))
|
||
|
||
# ---- 宠物 -----------------------------------------------------
|
||
def update_pet(self, character_id, pet_key="", level=None, exp=None,
|
||
satiety=None, clear_satiety=False, pet_index=None):
|
||
req = {"character_id": str(character_id)}
|
||
if pet_key:
|
||
req["pet_key"] = pet_key
|
||
if level is not None:
|
||
req["level"] = int(level)
|
||
if exp is not None:
|
||
req["exp"] = int(exp)
|
||
if satiety is not None:
|
||
req["satiety"] = int(satiety)
|
||
if clear_satiety:
|
||
req["clear_satiety"] = True
|
||
if pet_index is not None:
|
||
req["pet_index"] = int(pet_index)
|
||
return self._unwrap(self._post("/local/dnf-gm/pet/update", req))
|
||
|
||
# ---- 启动器工具扩展(换装 / 服务端工具 / 支援兵)----------------
|
||
def loadout_characters(self):
|
||
return self._unwrap(self._get("/local/dnf-gm/characters")) or []
|
||
|
||
def loadout_equipment(self, character_id):
|
||
qs = urlencode({"character_id": str(character_id)})
|
||
return self._unwrap(self._get("/local/dnf-gm/loadout/equipment?" + qs))
|
||
|
||
def loadout_apply(self, character_id, slots):
|
||
return self._unwrap(self._post("/local/dnf-gm/loadout/apply", {
|
||
"character_id": str(character_id), "slots": slots}))
|
||
|
||
def get_server_features(self):
|
||
return self._unwrap(self._get("/dnf/gm/features"))
|
||
|
||
def save_server_features(self, features):
|
||
return self._unwrap(self._post("/dnf/gm/features", features))
|
||
|
||
def companion_state(self, character_id):
|
||
qs = urlencode({"characterId": str(character_id)})
|
||
return self._unwrap(self._get("/local/companion?" + qs))
|
||
|
||
def companion_save(self, payload):
|
||
return self._unwrap(self._post("/local/companion", payload))
|