DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

+637
View File
@@ -0,0 +1,637 @@
# -*- 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))