家具实现
This commit is contained in:
1 parent
cae9bcb19e
commit
1413a3abc6
19 files changed
+1076
-9480
No files matched your search
@@ -0,0 +1,266 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
================================================================================
|
||||
家具家私分类转换规则数据
|
||||
================================================================================
|
||||
统一管理"家具家私"(xy_furniture) 分类下的全部【右键切换型】转换规则:
|
||||
箭桶、水槽、盘子架、书柜、厨具架、古式唱片机、垃圾桶。
|
||||
全部为即时右键转换, 无计时, 不需要 TimerService。
|
||||
由 xiangyemod/jiaju/service/FurnitureService 处理, 注册到 FeatureDispatcher。
|
||||
|
||||
【规则字段说明】
|
||||
source: 被右键的源方块
|
||||
item: 手持物品名; None=空手; "*music_disc"=任意原版唱片;
|
||||
"*arrow"=任意箭; "*any"=任意非空物品
|
||||
target: 转换成的目标方块
|
||||
consume: 消耗数量(默认0, 即不消耗)
|
||||
clearHand: True=清空主手全部物品(垃圾桶扔垃圾)
|
||||
giveItem: 取回/取水时给玩家背包的物品名(直接进背包)
|
||||
dropItem: 以掉落物形式给的物品 [{"name": "xx", "count": N}]
|
||||
record: "count"=记录一种物品+数量(箭桶) / "stack"=后放先取堆栈(书柜),
|
||||
记录写入方块实体数据(rtdd_swt/rtdd_swc/rtdd_swi/rtdd_swa/rtdd_sws),
|
||||
退出重进不丢
|
||||
maxCount: record 类型最大容量(箭桶32 / 书柜13)
|
||||
arrowMode:"store"=箭桶存箭(批量) / "retrieve"=箭桶取箭(一次性全取)
|
||||
discPut: True=唱片机放入时把唱片写入方块实体(取回时原样返还)
|
||||
discGet: True=唱片机取回时从方块实体读出唱片并掉落
|
||||
tip: 转换成功提示
|
||||
================================================================================
|
||||
"""
|
||||
|
||||
# ============================================================
|
||||
# 一、箭桶 (arrowbarrel_0 无箭 / arrowbarrel_1 有箭)
|
||||
# 存箭: 手持箭(普通箭/药水箭)右键, 批量存入手中所有箭, 至多32只,
|
||||
# 只存一种箭(类型+药水效果aux不符不能混存), count记录
|
||||
# 取箭: 空手右键, 一次性取回方块记录的全部箭
|
||||
# ============================================================
|
||||
ARROWBARREL_RULES = [
|
||||
{"source": "rtdd:arrowbarrel_0", "item": "*arrow",
|
||||
"target": "rtdd:arrowbarrel_1", "arrowMode": "store",
|
||||
"record": "count", "maxCount": 32,
|
||||
"tip": "§a已存入箭"},
|
||||
{"source": "rtdd:arrowbarrel_1", "item": None,
|
||||
"target": "rtdd:arrowbarrel_0", "arrowMode": "retrieve",
|
||||
"record": "count",
|
||||
"tip": "§a取回了箭"},
|
||||
]
|
||||
|
||||
# ============================================================
|
||||
# 二、水槽 (flume_0~5)
|
||||
# 加水: 空手右键空槽 -> 满水
|
||||
# 取水: 手持空水桶右键满水 -> 空槽 + 给装满水的水桶
|
||||
# 放脏盘子(脏盘子 rtdd:plate_0a): 满水起, 逐个放入最多4个
|
||||
# 取脏盘子: 空手右键逐个取回
|
||||
# ============================================================
|
||||
FLUME_RULES = [
|
||||
# 加水/取水
|
||||
{"source": "rtdd:flume_0", "item": None,
|
||||
"target": "rtdd:flume_1", "tip": "§a水槽加满了水"},
|
||||
{"source": "rtdd:flume_1", "item": "minecraft:bucket",
|
||||
"target": "rtdd:flume_0", "consume": 1,
|
||||
"giveItem": "minecraft:water_bucket", "tip": "§a取走了水, 水桶装满水"},
|
||||
# 放脏盘子(需满水)
|
||||
{"source": "rtdd:flume_1", "item": "rtdd:plate_0a",
|
||||
"target": "rtdd:flume_2", "consume": 1, "tip": "§a放入1个脏盘子"},
|
||||
{"source": "rtdd:flume_2", "item": "rtdd:plate_0a",
|
||||
"target": "rtdd:flume_3", "consume": 1, "tip": "§a放入1个脏盘子"},
|
||||
{"source": "rtdd:flume_3", "item": "rtdd:plate_0a",
|
||||
"target": "rtdd:flume_4", "consume": 1, "tip": "§a放入1个脏盘子"},
|
||||
{"source": "rtdd:flume_4", "item": "rtdd:plate_0a",
|
||||
"target": "rtdd:flume_5", "consume": 1, "tip": "§a放入1个脏盘子"},
|
||||
# 取脏盘子
|
||||
{"source": "rtdd:flume_5", "item": None,
|
||||
"target": "rtdd:flume_4", "giveItem": "rtdd:plate_0a", "tip": "§a取回1个脏盘子"},
|
||||
{"source": "rtdd:flume_4", "item": None,
|
||||
"target": "rtdd:flume_3", "giveItem": "rtdd:plate_0a", "tip": "§a取回1个脏盘子"},
|
||||
{"source": "rtdd:flume_3", "item": None,
|
||||
"target": "rtdd:flume_2", "giveItem": "rtdd:plate_0a", "tip": "§a取回1个脏盘子"},
|
||||
{"source": "rtdd:flume_2", "item": None,
|
||||
"target": "rtdd:flume_1", "giveItem": "rtdd:plate_0a", "tip": "§a取回1个脏盘子"},
|
||||
]
|
||||
|
||||
# ============================================================
|
||||
# 三、盘子架 (trayracks_1 空 ~ trayracks_6 五个)
|
||||
# 放盘子(盘子 rtdd:plate_0): 逐个放入最多5个
|
||||
# 取盘子: 空手右键逐个取回
|
||||
# ============================================================
|
||||
TRAYRACKS_RULES = [
|
||||
{"source": "rtdd:trayracks_1", "item": "rtdd:plate_0",
|
||||
"target": "rtdd:trayracks_2", "consume": 1, "tip": "§a放上1个盘子"},
|
||||
{"source": "rtdd:trayracks_2", "item": "rtdd:plate_0",
|
||||
"target": "rtdd:trayracks_3", "consume": 1, "tip": "§a放上1个盘子"},
|
||||
{"source": "rtdd:trayracks_3", "item": "rtdd:plate_0",
|
||||
"target": "rtdd:trayracks_4", "consume": 1, "tip": "§a放上1个盘子"},
|
||||
{"source": "rtdd:trayracks_4", "item": "rtdd:plate_0",
|
||||
"target": "rtdd:trayracks_5", "consume": 1, "tip": "§a放上1个盘子"},
|
||||
{"source": "rtdd:trayracks_5", "item": "rtdd:plate_0",
|
||||
"target": "rtdd:trayracks_6", "consume": 1, "tip": "§a放上1个盘子"},
|
||||
{"source": "rtdd:trayracks_6", "item": None,
|
||||
"target": "rtdd:trayracks_5", "giveItem": "rtdd:plate_0", "tip": "§a取回1个盘子"},
|
||||
{"source": "rtdd:trayracks_5", "item": None,
|
||||
"target": "rtdd:trayracks_4", "giveItem": "rtdd:plate_0", "tip": "§a取回1个盘子"},
|
||||
{"source": "rtdd:trayracks_4", "item": None,
|
||||
"target": "rtdd:trayracks_3", "giveItem": "rtdd:plate_0", "tip": "§a取回1个盘子"},
|
||||
{"source": "rtdd:trayracks_3", "item": None,
|
||||
"target": "rtdd:trayracks_2", "giveItem": "rtdd:plate_0", "tip": "§a取回1个盘子"},
|
||||
{"source": "rtdd:trayracks_2", "item": None,
|
||||
"target": "rtdd:trayracks_1", "giveItem": "rtdd:plate_0", "tip": "§a取回1个盘子"},
|
||||
]
|
||||
|
||||
# ============================================================
|
||||
# 四、书柜 (bookcase 系列, 后放先取, 至多13本)
|
||||
# 放书(书 minecraft:book / 附魔书 minecraft:enchanted_book): 逐个放入,
|
||||
# stack记录完整物品信息(附魔书附魔不丢)
|
||||
# 取书: 空手右键, 后放先取(LIFO)
|
||||
# 方块链: 书柜_1(空)=bookcase, 书柜_2(1本)=bookcase_12, 书柜_3(2本)=bookcase_11,
|
||||
# 书柜_4(3本)=bookcase_10, 书柜_5(4本)=bookcase_9, 书柜_6(5本)=bookcase_8,
|
||||
# 书柜_7(6本)=bookcase_7, 书柜_8(7本)=bookcase_6, 书柜_9(8本)=bookcase_5,
|
||||
# 书柜_10(9本)=bookcase_4, 书柜_11(10本)=b_196, 书柜_12(11本)=bookcase_3,
|
||||
# 书柜_13(12本)=bookcase_2, 书柜_14(13本)=bookcase_1
|
||||
# ============================================================
|
||||
_BOOKCASE_CHAIN = [
|
||||
"rtdd:bookcase", # 书柜_1(空)
|
||||
"rtdd:bookcase_12", # 书柜_2(1本)
|
||||
"rtdd:bookcase_11", # 书柜_3(2本)
|
||||
"rtdd:bookcase_10", # 书柜_4(3本)
|
||||
"rtdd:bookcase_9", # 书柜_5(4本)
|
||||
"rtdd:bookcase_8", # 书柜_6(5本)
|
||||
"rtdd:bookcase_7", # 书柜_7(6本)
|
||||
"rtdd:bookcase_6", # 书柜_8(7本)
|
||||
"rtdd:bookcase_5", # 书柜_9(8本)
|
||||
"rtdd:bookcase_4", # 书柜_10(9本)
|
||||
"rtdd:b_196", # 书柜_11(10本)
|
||||
"rtdd:bookcase_3", # 书柜_12(11本)
|
||||
"rtdd:bookcase_2", # 书柜_13(12本)
|
||||
"rtdd:bookcase_1", # 书柜_14(13本)
|
||||
]
|
||||
|
||||
BOOKCASE_RULES = []
|
||||
# 放入: 第 i 级 -> 第 i+1 级, 书/附魔书, 消耗1, stack记录
|
||||
for _i in range(len(_BOOKCASE_CHAIN) - 1):
|
||||
_src, _dst = _BOOKCASE_CHAIN[_i], _BOOKCASE_CHAIN[_i + 1]
|
||||
BOOKCASE_RULES.append({
|
||||
"source": _src, "item": "minecraft:book", "target": _dst,
|
||||
"consume": 1, "record": "stack", "maxCount": 13,
|
||||
"tip": "§a放入1本书",
|
||||
})
|
||||
BOOKCASE_RULES.append({
|
||||
"source": _src, "item": "minecraft:enchanted_book", "target": _dst,
|
||||
"consume": 1, "record": "stack", "maxCount": 13,
|
||||
"tip": "§a放入1本附魔书",
|
||||
})
|
||||
# 取回: 第 i 级 -> 第 i-1 级, 空手, 后放先取
|
||||
for _i in range(len(_BOOKCASE_CHAIN) - 1, 0, -1):
|
||||
_src, _dst = _BOOKCASE_CHAIN[_i], _BOOKCASE_CHAIN[_i - 1]
|
||||
BOOKCASE_RULES.append({
|
||||
"source": _src, "item": None, "target": _dst,
|
||||
"record": "stack", "tip": "§a取回了书",
|
||||
})
|
||||
|
||||
# ============================================================
|
||||
# 五、厨具架 (kitchenwarerack, 至多挂2件, 方块状态即组合, 纯切换)
|
||||
# 可放: 锅铲(spatula) / 锅勺(potandspoon) / 菜刀(cleaver) / 炒锅(wok)
|
||||
# ============================================================
|
||||
KITCHENRACK_RULES = [
|
||||
# ---- 放入 (空架 -> 单件) ----
|
||||
{"source": "rtdd:kitchenwarerack", "item": "rtdd:spatula",
|
||||
"target": "rtdd:kitchenwareracka_1", "consume": 1, "tip": "§a放上了锅铲"},
|
||||
{"source": "rtdd:kitchenwarerack", "item": "rtdd:potandspoon",
|
||||
"target": "rtdd:kitchenwarerackb_1", "consume": 1, "tip": "§a放上了锅勺"},
|
||||
{"source": "rtdd:kitchenwarerack", "item": "rtdd:cleaver",
|
||||
"target": "rtdd:kitchenwarerackd_1", "consume": 1, "tip": "§a放上了菜刀"},
|
||||
{"source": "rtdd:kitchenwarerack", "item": "rtdd:wok",
|
||||
"target": "rtdd:kitchenwarerackc_1", "consume": 1, "tip": "§a放上了炒锅"},
|
||||
# ---- 放入 (单件 -> 双件/组合) ----
|
||||
{"source": "rtdd:kitchenwareracka_1", "item": "rtdd:spatula",
|
||||
"target": "rtdd:kitchenwareracka_2", "consume": 1, "tip": "§a再放上一把锅铲"},
|
||||
{"source": "rtdd:kitchenwareracka_1", "item": "rtdd:potandspoon",
|
||||
"target": "rtdd:kitchenwareracka_3", "consume": 1, "tip": "§a放上了锅勺"},
|
||||
{"source": "rtdd:kitchenwarerackb_1", "item": "rtdd:potandspoon",
|
||||
"target": "rtdd:kitchenwarerackb_2", "consume": 1, "tip": "§a再放上一把锅勺"},
|
||||
{"source": "rtdd:kitchenwarerackc_1", "item": "rtdd:cleaver",
|
||||
"target": "rtdd:kitchenwarerackc_2", "consume": 1, "tip": "§a放上了菜刀"},
|
||||
{"source": "rtdd:kitchenwarerackd_1", "item": "rtdd:cleaver",
|
||||
"target": "rtdd:kitchenwarerackd_3", "consume": 1, "tip": "§a再放上一把菜刀"},
|
||||
{"source": "rtdd:kitchenwarerackd_1", "item": "rtdd:spatula",
|
||||
"target": "rtdd:kitchenwarerackd_2", "consume": 1, "tip": "§a放上了锅铲"},
|
||||
{"source": "rtdd:kitchenwarerackd_1", "item": "rtdd:potandspoon",
|
||||
"target": "rtdd:kitchenwarerackd_4", "consume": 1, "tip": "§a放上了锅勺"},
|
||||
# ---- 取回 (空手右键, 掉落对应厨具) ----
|
||||
{"source": "rtdd:kitchenwareracka_1", "item": None,
|
||||
"target": "rtdd:kitchenwarerack",
|
||||
"dropItem": [{"name": "rtdd:spatula", "count": 1}], "tip": "§a取回了锅铲"},
|
||||
{"source": "rtdd:kitchenwareracka_2", "item": None,
|
||||
"target": "rtdd:kitchenwareracka_1",
|
||||
"dropItem": [{"name": "rtdd:spatula", "count": 1}], "tip": "§a取回了一把锅铲"},
|
||||
{"source": "rtdd:kitchenwareracka_3", "item": None,
|
||||
"target": "rtdd:kitchenwareracka_1",
|
||||
"dropItem": [{"name": "rtdd:potandspoon", "count": 1}], "tip": "§a取回了锅勺"},
|
||||
{"source": "rtdd:kitchenwarerackb_1", "item": None,
|
||||
"target": "rtdd:kitchenwarerack",
|
||||
"dropItem": [{"name": "rtdd:potandspoon", "count": 1}], "tip": "§a取回了锅勺"},
|
||||
{"source": "rtdd:kitchenwarerackb_2", "item": None,
|
||||
"target": "rtdd:kitchenwarerackb_1",
|
||||
"dropItem": [{"name": "rtdd:potandspoon", "count": 1}], "tip": "§a取回了一把锅勺"},
|
||||
{"source": "rtdd:kitchenwarerackc_1", "item": None,
|
||||
"target": "rtdd:kitchenwarerack",
|
||||
"dropItem": [{"name": "rtdd:wok", "count": 1}], "tip": "§a取回了炒锅"},
|
||||
{"source": "rtdd:kitchenwarerackc_2", "item": None,
|
||||
"target": "rtdd:kitchenwarerackc_1",
|
||||
"dropItem": [{"name": "rtdd:cleaver", "count": 1}], "tip": "§a取回了菜刀"},
|
||||
{"source": "rtdd:kitchenwarerackd_1", "item": None,
|
||||
"target": "rtdd:kitchenwarerack",
|
||||
"dropItem": [{"name": "rtdd:cleaver", "count": 1}], "tip": "§a取回了菜刀"},
|
||||
{"source": "rtdd:kitchenwarerackd_2", "item": None,
|
||||
"target": "rtdd:kitchenwarerackd_1",
|
||||
"dropItem": [{"name": "rtdd:spatula", "count": 1}], "tip": "§a取回了锅铲"},
|
||||
{"source": "rtdd:kitchenwarerackd_3", "item": None,
|
||||
"target": "rtdd:kitchenwarerackd_1",
|
||||
"dropItem": [{"name": "rtdd:cleaver", "count": 1}], "tip": "§a取回了一把菜刀"},
|
||||
{"source": "rtdd:kitchenwarerackd_4", "item": None,
|
||||
"target": "rtdd:kitchenwarerackd_1",
|
||||
"dropItem": [{"name": "rtdd:potandspoon", "count": 1}], "tip": "§a取回了锅勺"},
|
||||
]
|
||||
|
||||
# ============================================================
|
||||
# 六、古式唱片机 (b_108 无碟 / b_109 有碟)
|
||||
# 放入: 手持任意原版唱片 -> 有碟, 消耗1, 记录唱片(rtdd_disc)
|
||||
# 取回: 空手右键 -> 无碟 + 原样返还唱片
|
||||
# 注: 放入后播放曲目为 todo, 后续实现
|
||||
# ============================================================
|
||||
RECORDPLAYER_RULES = [
|
||||
{"source": "rtdd:b_108", "item": "*music_disc",
|
||||
"target": "rtdd:b_109", "consume": 1, "discPut": True,
|
||||
"tip": "§a放入了唱片"},
|
||||
{"source": "rtdd:b_109", "item": None,
|
||||
"target": "rtdd:b_108", "discGet": True,
|
||||
"tip": "§a取回了唱片"},
|
||||
]
|
||||
|
||||
# ============================================================
|
||||
# 七、垃圾桶 (storage_buckets 关闭 / rubbish_bin_2 打开 / b_160 有垃圾)
|
||||
# 打开: 右键垃圾桶(空手/手持均可) -> 打开桶盖(不消耗)
|
||||
# 扔垃圾: 打开时手持任意物品右键 -> 有垃圾 + 清空主手
|
||||
# 关闭: 右键(空手/手持均可) 打开/有垃圾 -> 关闭
|
||||
# ============================================================
|
||||
TRASHBIN_RULES = [
|
||||
{"source": "rtdd:storage_buckets", "item": None,
|
||||
"target": "rtdd:rubbish_bin_2", "tip": "§a垃圾桶已打开"},
|
||||
{"source": "rtdd:storage_buckets", "item": "*any",
|
||||
"target": "rtdd:rubbish_bin_2", "tip": "§a垃圾桶已打开"},
|
||||
{"source": "rtdd:rubbish_bin_2", "item": None,
|
||||
"target": "rtdd:storage_buckets", "tip": "§a垃圾桶已关闭"},
|
||||
{"source": "rtdd:rubbish_bin_2", "item": "*any",
|
||||
"target": "rtdd:b_160", "clearHand": True, "tip": "§a已丢入垃圾桶"},
|
||||
{"source": "rtdd:b_160", "item": None,
|
||||
"target": "rtdd:storage_buckets", "tip": "§a垃圾桶已关闭"},
|
||||
{"source": "rtdd:b_160", "item": "*any",
|
||||
"target": "rtdd:storage_buckets", "tip": "§a垃圾桶已关闭"},
|
||||
]
|
||||
|
||||
# ============================================================
|
||||
# 汇总: 家具家私全部右键转换规则
|
||||
# ============================================================
|
||||
FURNITURE_SWITCHES = (ARROWBARREL_RULES + FLUME_RULES + TRAYRACKS_RULES
|
||||
+ BOOKCASE_RULES + KITCHENRACK_RULES
|
||||
+ RECORDPLAYER_RULES + TRASHBIN_RULES)
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
+603
@@ -0,0 +1,603 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
================================================================================
|
||||
家具家私·右键转换引擎(服务端)
|
||||
================================================================================
|
||||
处理"家具家私"(xy_furniture) 分类下的全部【右键切换型】转换:
|
||||
箭桶(存/取箭)、水槽(加水/取水/放洗脏盘)、盘子架(放/取盘)、
|
||||
书柜(放/取书,后放先取)、厨具架(放/取厨具)、古式唱片机(放/取唱片)、
|
||||
垃圾桶(开/关/扔垃圾)。
|
||||
全部为即时右键转换, 无计时, 因此只有一个服务类, 不需要拆分 TimerService。
|
||||
|
||||
【事件监听在哪?】
|
||||
本类【不】监听任何事件!
|
||||
所有事件由 FeatureDispatcher(xiangyemod/service/FeatureDispatcher.py)统一监听:
|
||||
- ServerBlockUseEvent → OnBlockUse → 按 blockOwner 路由到本类 handleBlockUse
|
||||
本类只实现被分发器调用的接口方法, 不关心事件来源。
|
||||
|
||||
【规则匹配】
|
||||
findRecipe 支持五种:
|
||||
1. 精确匹配 (方块名, 物品名) —— 水槽/盘子架/厨具架/书柜等
|
||||
2. 唱片机 (方块名, "*music_disc") —— 手持任意原版唱片
|
||||
3. 箭桶 (方块名, "*arrow") —— 手持任意箭(普通箭/药水箭)
|
||||
4. 任意物品(方块名, "*any") —— 垃圾桶: 手持任意非空物品右键
|
||||
5. 空手 (方块名, None) —— 取回/开关类, 只有真正空手才 fallback
|
||||
|
||||
【方块记录(存储物品)】
|
||||
记录写入方块实体数据, 退出重进不丢(rtdd_swt 类型标记):
|
||||
- count记录(箭桶): 记录一种箭的类型+数量, 字段 rtdd_swt=count /
|
||||
rtdd_swc=数量 / rtdd_swi=物品名 / rtdd_swa=aux
|
||||
- stack记录(书柜): 后放先取堆栈, 记录完整物品信息(含userData, 附魔不丢),
|
||||
字段 rtdd_swt=stack / rtdd_sws=堆栈列表
|
||||
唱片机单独用 rtdd_disc 记录唱片。SetBlockNew 后 block entity 可能未初始化,
|
||||
写入带 0.1s 延迟兜底。
|
||||
|
||||
【本类怎么做(自上而下的方法系列)】
|
||||
handleBlockUse ← 入口:分发器转发右键方块事件
|
||||
├─ findRecipe ← 匹配规则
|
||||
├─ checkCooldown← 连点冷却
|
||||
├─ _handleArrowMode ← 箭桶批量存/取箭
|
||||
└─ handleSwitch ← 其他切换: 读记录→消耗/清手→替换→记记录→给/掉物品→提示
|
||||
================================================================================
|
||||
"""
|
||||
import time
|
||||
import mod.server.extraServerApi as serverApi
|
||||
|
||||
# ---- 网易API基础对象 ----
|
||||
ServerSystem = serverApi.GetServerSystemCls() # 服务端系统基类
|
||||
comp = serverApi.GetEngineCompFactory() # 组件工厂
|
||||
|
||||
# ---- 继承 BlueFox 框架的系统基类(提供 CreateEngineItemEntity 等方法) ----
|
||||
from ....BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxServerSystem
|
||||
from ....BlueFoxTools.ToolsConfig import LevelId
|
||||
|
||||
# ---- 方块实体数据组件(记录箭桶数量/书柜堆栈/唱片) ----
|
||||
blockEntityDataComp = comp.CreateBlockEntityData(LevelId)
|
||||
# ---- 游戏组件(AddTimer 延迟写入 block entity 兜底) ----
|
||||
gameComp = comp.CreateGame(LevelId)
|
||||
|
||||
# ---- 只导入家具家私自己的数据库 ----
|
||||
from ..FurnitureData import FURNITURE_SWITCHES
|
||||
|
||||
# ---- 统一分发器 ----
|
||||
from ...service.FeatureDispatcher import FeatureDispatcher
|
||||
|
||||
|
||||
class FurnitureService(BlueFoxServerSystem):
|
||||
"""
|
||||
家具家私·右键转换引擎
|
||||
|
||||
【核心数据结构】
|
||||
furnitureRecipes = {(方块名, 物品名): 规则dict} # O(1) 查找
|
||||
furnitureBlockSet = {方块名, ...} # O(1) 注册用
|
||||
cooldown = {key: 时间戳} # 连点冷却表
|
||||
"""
|
||||
|
||||
def __init__(self, namespace, systemName):
|
||||
"""
|
||||
初始化:构建规则字典,并注册到统一分发器。
|
||||
"""
|
||||
super(FurnitureService, self).__init__(namespace, systemName)
|
||||
print "===== FurnitureService init ====="
|
||||
|
||||
# ---- 构建规则字典(O(1) 查找) ----
|
||||
self.furnitureRecipes = {}
|
||||
self.furnitureBlockSet = set()
|
||||
for sw in FURNITURE_SWITCHES:
|
||||
key = (sw["source"], sw.get("item"))
|
||||
self.furnitureRecipes[key] = sw
|
||||
self.furnitureBlockSet.add(sw["source"])
|
||||
|
||||
# ---- 右键冷却表 ----
|
||||
self.cooldown = {}
|
||||
|
||||
# ---- 注册到统一分发器 ----
|
||||
FeatureDispatcher.register(self)
|
||||
|
||||
# ============================================================================
|
||||
# 一、给 FeatureDispatcher 的接口
|
||||
# ============================================================================
|
||||
|
||||
def getFeatureBlocks(self):
|
||||
"""返回本功能管辖的所有方块名集合(右键路由用)"""
|
||||
return self.furnitureBlockSet
|
||||
|
||||
def getItemUseOnRules(self):
|
||||
"""返回"手持物品右键方块表面"的规则(家具家私没有, 返回空)"""
|
||||
return {}
|
||||
|
||||
def handleBlockUse(self, args):
|
||||
"""
|
||||
由 FeatureDispatcher 转发的右键方块入口。
|
||||
只有 blockName 在本功能的方块集合里才会被调用。
|
||||
|
||||
返回 True = 本功能处理了这个右键(分发器取消默认行为)
|
||||
返回 False = 当前组合没有匹配规则(分发器不拦截)
|
||||
"""
|
||||
blockName = args.get("blockName")
|
||||
playerId = args.get("playerId")
|
||||
x, y, z = args.get("x"), args.get("y"), args.get("z")
|
||||
pos = (x, y, z)
|
||||
dim = comp.CreateDimension(playerId).GetEntityDimensionId()
|
||||
key = (dim, x, y, z)
|
||||
|
||||
itemDict = self.getCarriedItem(playerId)
|
||||
|
||||
recipe = self.findRecipe(blockName, itemDict)
|
||||
if not recipe:
|
||||
return False
|
||||
|
||||
# 连点冷却
|
||||
if not self.checkCooldown(key, recipe.get("switchCd", 0.5)):
|
||||
return True
|
||||
|
||||
# 箭桶模式:批量存/取箭, 单独处理
|
||||
if recipe.get("arrowMode"):
|
||||
self._handleArrowMode(recipe, playerId, pos, dim, itemDict, key)
|
||||
return True
|
||||
|
||||
# 其余切换型规则:就地执行
|
||||
self.handleSwitch(recipe, playerId, pos, dim, itemDict, key)
|
||||
return True
|
||||
|
||||
# ============================================================================
|
||||
# 二、规则匹配(O(1) 字典查找)
|
||||
# ============================================================================
|
||||
|
||||
def findRecipe(self, blockName, itemDict):
|
||||
"""
|
||||
匹配规则, 优先级从高到低:
|
||||
1. 精确匹配 (方块名, 物品名)
|
||||
2. 唱片机 (方块名, "*music_disc")
|
||||
3. 箭桶 (方块名, "*arrow")
|
||||
4. 任意物品 (方块名, "*any")
|
||||
5. 空手 (方块名, None) —— 只有真正空手才 fallback
|
||||
"""
|
||||
itemName = itemDict.get("newItemName") if itemDict else None
|
||||
|
||||
# 1. 精确匹配
|
||||
recipe = self.furnitureRecipes.get((blockName, itemName))
|
||||
if recipe:
|
||||
return recipe
|
||||
|
||||
# 2. 唱片机:手持任意原版唱片
|
||||
if itemName and itemName.startswith("minecraft:music_disc"):
|
||||
recipe = self.furnitureRecipes.get((blockName, "*music_disc"))
|
||||
if recipe:
|
||||
return recipe
|
||||
|
||||
# 3. 箭桶:手持任意箭(普通箭/药水箭)
|
||||
if itemName in ("minecraft:arrow", "minecraft:tipped_arrow"):
|
||||
recipe = self.furnitureRecipes.get((blockName, "*arrow"))
|
||||
if recipe:
|
||||
return recipe
|
||||
|
||||
# 4. 任意物品匹配(垃圾桶)
|
||||
if itemName and itemName != "minecraft:air":
|
||||
recipe = self.furnitureRecipes.get((blockName, "*any"))
|
||||
if recipe:
|
||||
return recipe
|
||||
|
||||
# 5. 空手匹配
|
||||
if not itemName or itemName == "minecraft:air":
|
||||
return self.furnitureRecipes.get((blockName, None))
|
||||
|
||||
return None
|
||||
|
||||
# ============================================================================
|
||||
# 三、箭桶模式(批量存箭/一次性取箭, count记录)
|
||||
# ============================================================================
|
||||
|
||||
def _handleArrowMode(self, recipe, playerId, pos, dim, itemDict, key):
|
||||
"""
|
||||
箭桶存/取箭:
|
||||
存箭: 批量存入手中所有箭, 至多 maxCount(32), 只存一种箭
|
||||
(方块内已有箭且与手中类型/药水效果不同则不能混存)
|
||||
取箭: 一次性取回方块记录的全部箭(数量+类型+药水效果aux)
|
||||
"""
|
||||
mode = recipe.get("arrowMode")
|
||||
|
||||
if mode == "store":
|
||||
# ---- 存箭 ----
|
||||
if not itemDict:
|
||||
return
|
||||
handCount = itemDict.get("count", 0)
|
||||
if handCount <= 0:
|
||||
return
|
||||
handItemName = itemDict.get("newItemName", "")
|
||||
handAux = itemDict.get("newAuxValue", 0)
|
||||
|
||||
record = self._loadRecord(pos, dim)
|
||||
# 类型检查: 已有箭且类型不符 -> 不能混存
|
||||
if record and record.get("count", 0) > 0:
|
||||
if (record.get("itemName") != handItemName
|
||||
or record.get("aux", 0) != handAux):
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(
|
||||
playerId, "§c箭桶里已有其他种类的箭, 不能混存!")
|
||||
return
|
||||
else:
|
||||
record = {"count": 0, "itemName": handItemName, "aux": handAux}
|
||||
|
||||
# 计算实际可存数量
|
||||
maxCount = recipe.get("maxCount", 32)
|
||||
if record["count"] >= maxCount:
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(
|
||||
playerId, "§c箭桶已满!")
|
||||
return
|
||||
storeCount = min(handCount, maxCount - record["count"])
|
||||
|
||||
# 从手中扣减 storeCount 只箭
|
||||
self._consumeCount(playerId, itemDict, storeCount)
|
||||
|
||||
# 更新记录
|
||||
record["count"] += storeCount
|
||||
|
||||
# 替换方块
|
||||
if recipe.get("target"):
|
||||
self.replaceBlock(pos, dim, recipe["target"], playerId)
|
||||
|
||||
# 保存记录到新方块(延迟兜底)
|
||||
self._saveRecordDelayed(pos, dim, record)
|
||||
|
||||
if recipe.get("tip"):
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(
|
||||
playerId, "§a存入了 %d 只箭 (共%d/32)" % (storeCount, record["count"]))
|
||||
|
||||
elif mode == "retrieve":
|
||||
# ---- 取箭 ----
|
||||
record = self._loadRecord(pos, dim)
|
||||
if not record or record.get("count", 0) <= 0:
|
||||
return
|
||||
|
||||
retCount = record["count"]
|
||||
retItemName = record["itemName"]
|
||||
retAux = record.get("aux", 0)
|
||||
|
||||
# 给玩家箭
|
||||
self.giveItem(playerId, retItemName, retCount, retAux)
|
||||
|
||||
# 替换方块
|
||||
if recipe.get("target"):
|
||||
self.replaceBlock(pos, dim, recipe["target"], playerId)
|
||||
|
||||
# 清空记录
|
||||
self._clearRecord(pos, dim)
|
||||
|
||||
if recipe.get("tip"):
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(
|
||||
playerId, "§a取回了 %d 只箭" % retCount)
|
||||
|
||||
# ============================================================================
|
||||
# 四、切换型规则处理(水槽/盘子架/书柜/厨具架/唱片机/垃圾桶)
|
||||
# ============================================================================
|
||||
|
||||
def handleSwitch(self, recipe, playerId, pos, dim, itemDict, key):
|
||||
"""
|
||||
执行"右键直接转换"类规则:
|
||||
书柜取回/唱片机取回 → 先读记录
|
||||
消耗物品/清空主手 → 替换方块 → 记录放入 → 给/掉物品 → 提示
|
||||
"""
|
||||
# ---- 取回类: 先读记录(此时方块还是满的/有碟的) ----
|
||||
discToDrop = None
|
||||
if recipe.get("discGet"):
|
||||
discToDrop = self._readDiscData(pos, dim)
|
||||
self._clearDiscData(pos, dim)
|
||||
if recipe.get("record") == "stack" and recipe.get("item") is None:
|
||||
self._popStackAndGive(playerId, pos, dim)
|
||||
|
||||
# ---- 消耗物品 ----
|
||||
if recipe.get("clearHand"):
|
||||
self.clearHand(playerId)
|
||||
elif recipe.get("item") is not None and recipe.get("consume", 0) > 0:
|
||||
self.consumeItem(playerId, itemDict, recipe.get("consume", 1))
|
||||
|
||||
# ---- 替换方块 ----
|
||||
if recipe.get("target"):
|
||||
self.replaceBlock(pos, dim, recipe["target"], playerId)
|
||||
|
||||
# ---- 放入类: 记录到新方块(延迟兜底) ----
|
||||
if recipe.get("record") == "stack" and recipe.get("item") is not None and itemDict:
|
||||
self._pushStack(pos, dim, itemDict)
|
||||
if recipe.get("discPut") and itemDict:
|
||||
self._writeDiscData(dim, pos, itemDict)
|
||||
|
||||
# ---- 掉落物(厨具架取回/唱片取回) ----
|
||||
if discToDrop:
|
||||
self._dropItemDict(dim, pos, discToDrop)
|
||||
if recipe.get("dropItem"):
|
||||
self.dropItems(dim, pos, recipe["dropItem"])
|
||||
|
||||
# ---- 给玩家背包物品(水槽取水/取脏盘子/盘子架取回) ----
|
||||
if recipe.get("giveItem"):
|
||||
self.giveItem(playerId, recipe["giveItem"])
|
||||
|
||||
# ---- 转换成功提示 ----
|
||||
if recipe.get("tip"):
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(playerId, recipe["tip"])
|
||||
|
||||
# ============================================================================
|
||||
# 五、方块记录:count/stack 读写(箭桶/书柜, 字段 rtdd_sw*)
|
||||
# ============================================================================
|
||||
|
||||
def _loadRecord(self, pos, dim):
|
||||
"""从方块实体数据读出记录: count型 -> {"count","itemName","aux"},
|
||||
stack型 -> {"stack":[...]}; 无记录返回 None"""
|
||||
try:
|
||||
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
if blockData is None:
|
||||
return None
|
||||
swt = blockData["rtdd_swt"]
|
||||
if swt == "count":
|
||||
return {"count": blockData["rtdd_swc"],
|
||||
"itemName": blockData["rtdd_swi"],
|
||||
"aux": blockData["rtdd_swa"]}
|
||||
if swt == "stack":
|
||||
return {"stack": blockData["rtdd_sws"]}
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _saveRecord(self, pos, dim, record):
|
||||
"""把记录写入方块实体数据"""
|
||||
try:
|
||||
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
if blockData is None:
|
||||
return
|
||||
if "count" in record:
|
||||
blockData["rtdd_swt"] = "count"
|
||||
blockData["rtdd_swc"] = record["count"]
|
||||
blockData["rtdd_swi"] = record.get("itemName", "")
|
||||
blockData["rtdd_swa"] = record.get("aux", 0)
|
||||
elif "stack" in record:
|
||||
blockData["rtdd_swt"] = "stack"
|
||||
blockData["rtdd_sws"] = record["stack"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _saveRecordDelayed(self, pos, dim, record):
|
||||
"""写入记录, block entity 未初始化则 0.1s 延迟兜底"""
|
||||
try:
|
||||
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
if blockData is None:
|
||||
gameComp.AddTimer(0.1, self._saveRecord, pos, dim, record)
|
||||
return
|
||||
self._saveRecord(pos, dim, record)
|
||||
except Exception:
|
||||
gameComp.AddTimer(0.1, self._saveRecord, pos, dim, record)
|
||||
|
||||
def _clearRecord(self, pos, dim):
|
||||
"""清除方块实体数据里的记录"""
|
||||
try:
|
||||
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
if blockData is not None:
|
||||
blockData["rtdd_swt"] = ""
|
||||
blockData["rtdd_swc"] = 0
|
||||
blockData["rtdd_swi"] = ""
|
||||
blockData["rtdd_swa"] = 0
|
||||
blockData["rtdd_sws"] = []
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ---- 书柜 stack 记录: 压栈/弹栈 ----
|
||||
|
||||
def _pushStack(self, pos, dim, itemDict):
|
||||
"""书柜放入: 把书的完整信息压入堆栈(含userData, 附魔不丢)"""
|
||||
record = self._loadRecord(pos, dim)
|
||||
if not record:
|
||||
record = {"stack": []}
|
||||
record["stack"].append(self._toItemObj(itemDict))
|
||||
self._saveRecordDelayed(pos, dim, record)
|
||||
|
||||
def _popStackAndGive(self, playerId, pos, dim):
|
||||
"""书柜取回: 弹栈(后放先取)并把书给玩家"""
|
||||
record = self._loadRecord(pos, dim)
|
||||
if not record or not record.get("stack"):
|
||||
return
|
||||
topItem = record["stack"].pop()
|
||||
self.giveItemObj(playerId, topItem)
|
||||
if record["stack"]:
|
||||
self._saveRecordDelayed(pos, dim, record)
|
||||
else:
|
||||
self._clearRecord(pos, dim)
|
||||
|
||||
def _toItemObj(self, itemDict):
|
||||
"""从手持物品 dict 提取可持久化的物品对象"""
|
||||
return {
|
||||
"itemName": itemDict.get("newItemName", ""),
|
||||
"count": itemDict.get("count", 1),
|
||||
"aux": itemDict.get("newAuxValue", 0),
|
||||
"userData": itemDict.get("userData", {}),
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 六、唱片机唱片记录(rtdd_disc)
|
||||
# ============================================================================
|
||||
|
||||
def _writeDiscData(self, dim, pos, itemDict):
|
||||
"""把放入的唱片(物品名+aux)写入方块实体数据 rtdd_disc, 延迟兜底"""
|
||||
discInfo = {
|
||||
"itemName": itemDict.get("newItemName", "minecraft:music_disc_13"),
|
||||
"aux": itemDict.get("newAuxValue", 0),
|
||||
}
|
||||
try:
|
||||
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
if blockData is None:
|
||||
gameComp.AddTimer(0.1, self._writeDiscDataDelayed,
|
||||
dim, pos, discInfo)
|
||||
return
|
||||
blockData["rtdd_disc"] = discInfo
|
||||
except Exception:
|
||||
gameComp.AddTimer(0.1, self._writeDiscDataDelayed,
|
||||
dim, pos, discInfo)
|
||||
|
||||
def _writeDiscDataDelayed(self, dim, pos, discInfo):
|
||||
"""延迟写入兜底"""
|
||||
try:
|
||||
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
if blockData is not None:
|
||||
blockData["rtdd_disc"] = discInfo
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _readDiscData(self, pos, dim):
|
||||
"""从方块实体数据读出唱片信息, 没有返回 None"""
|
||||
try:
|
||||
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
if blockData is None:
|
||||
return None
|
||||
# 注意: BlockEntityData 需用 [] 访问, 键不存在会抛异常, 捕获后按无记录处理
|
||||
return blockData["rtdd_disc"]
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _clearDiscData(self, pos, dim):
|
||||
"""清除方块实体数据里的唱片记录"""
|
||||
try:
|
||||
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
if blockData is not None:
|
||||
blockData["rtdd_disc"] = None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _dropItemDict(self, dim, pos, itemDict):
|
||||
"""以掉落物形式生成一个物品(唱片取回)"""
|
||||
try:
|
||||
self.CreateEngineItemEntity(
|
||||
{"itemName": itemDict["itemName"],
|
||||
"count": 1,
|
||||
"auxValue": itemDict.get("aux", 0)},
|
||||
dim, (pos[0], pos[1] + 0.5, pos[2]))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ============================================================================
|
||||
# 七、工具方法:获取玩家主手物品
|
||||
# ============================================================================
|
||||
|
||||
def getCarriedItem(self, playerId):
|
||||
itemComp = comp.CreateItem(playerId)
|
||||
return itemComp.GetPlayerItem(
|
||||
serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0)
|
||||
|
||||
# ============================================================================
|
||||
# 八、工具方法:给玩家物品
|
||||
# ============================================================================
|
||||
|
||||
def giveItem(self, playerId, itemName, count=1, aux=0, userData=None):
|
||||
"""往玩家背包塞一个物品, 背包满则掉地上"""
|
||||
try:
|
||||
itemDict = {"itemName": itemName, "count": count, "auxValue": aux}
|
||||
if userData:
|
||||
itemDict["userData"] = userData
|
||||
comp.CreateItem(playerId).SpawnItemToPlayerInv(itemDict, playerId)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def giveItemObj(self, playerId, itemObj):
|
||||
"""按持久化物品对象给玩家(书柜弹栈取回, 保留附魔)"""
|
||||
try:
|
||||
itemDict = {"itemName": itemObj["itemName"],
|
||||
"count": itemObj.get("count", 1),
|
||||
"auxValue": itemObj.get("aux", 0)}
|
||||
if itemObj.get("userData"):
|
||||
itemDict["userData"] = itemObj["userData"]
|
||||
comp.CreateItem(playerId).SpawnItemToPlayerInv(itemDict, playerId)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ============================================================================
|
||||
# 九、工具方法:消耗物品
|
||||
# ============================================================================
|
||||
|
||||
def consumeItem(self, playerId, itemDict, amount=1):
|
||||
if itemDict is None:
|
||||
return
|
||||
count = itemDict.get("count", 0)
|
||||
if count <= amount:
|
||||
comp.CreateItem(playerId).SetEntityItem(
|
||||
serverApi.GetMinecraftEnum().ItemPosType.CARRIED,
|
||||
{"itemName": "minecraft:air", "count": 0, "auxValue": 0})
|
||||
else:
|
||||
itemDict["count"] = count - amount
|
||||
comp.CreateItem(playerId).SetEntityItem(
|
||||
serverApi.GetMinecraftEnum().ItemPosType.CARRIED,
|
||||
{"itemName": itemDict["newItemName"],
|
||||
"count": itemDict["count"],
|
||||
"auxValue": itemDict.get("newAuxValue", 0)})
|
||||
|
||||
def _consumeCount(self, playerId, itemDict, amount):
|
||||
"""批量扣减指定数量(箭桶存箭)"""
|
||||
if not itemDict or amount <= 0:
|
||||
return
|
||||
count = itemDict.get("count", 0)
|
||||
if count <= amount:
|
||||
comp.CreateItem(playerId).SetEntityItem(
|
||||
serverApi.GetMinecraftEnum().ItemPosType.CARRIED,
|
||||
{"itemName": "minecraft:air", "count": 0, "auxValue": 0})
|
||||
else:
|
||||
itemDict["count"] = count - amount
|
||||
comp.CreateItem(playerId).SetEntityItem(
|
||||
serverApi.GetMinecraftEnum().ItemPosType.CARRIED,
|
||||
{"itemName": itemDict["newItemName"],
|
||||
"count": itemDict["count"],
|
||||
"auxValue": itemDict.get("newAuxValue", 0)})
|
||||
|
||||
def clearHand(self, playerId):
|
||||
"""清空主手全部物品(垃圾桶扔垃圾)"""
|
||||
comp.CreateItem(playerId).SetEntityItem(
|
||||
serverApi.GetMinecraftEnum().ItemPosType.CARRIED,
|
||||
{"itemName": "minecraft:air", "count": 0, "auxValue": 0})
|
||||
|
||||
# ============================================================================
|
||||
# 十、工具方法:替换方块
|
||||
# ============================================================================
|
||||
|
||||
def replaceBlock(self, pos, dim, newBlockName, playerId=None):
|
||||
"""
|
||||
替换指定位置的方块。
|
||||
playerId 可为 None, 用 LevelId 创建组件。
|
||||
"""
|
||||
creator = playerId if playerId is not None else LevelId
|
||||
aux = 0
|
||||
try:
|
||||
oldBlock = comp.CreateBlockInfo(creator).GetBlockNew(pos, dim)
|
||||
aux = oldBlock.get("aux", 0)
|
||||
except Exception:
|
||||
aux = 0
|
||||
comp.CreateBlockInfo(creator).SetBlockNew(
|
||||
pos,
|
||||
blockDict={"name": newBlockName, "aux": aux},
|
||||
oldBlockHandling=0,
|
||||
dimensionId=dim)
|
||||
|
||||
# ============================================================================
|
||||
# 十一、工具方法:以掉落物形式生成物品(厨具架取回)
|
||||
# ============================================================================
|
||||
|
||||
def dropItems(self, dim, pos, dropList):
|
||||
"""
|
||||
dropList 格式:[{"name": "xxx", "count": N}, ...]
|
||||
在方块上方生成掉落物实体, 让它自然掉到地上。
|
||||
"""
|
||||
for d in dropList:
|
||||
itemDict = {
|
||||
"itemName": d["name"],
|
||||
"count": d.get("count", 1),
|
||||
"auxValue": d.get("aux", 0),
|
||||
}
|
||||
self.CreateEngineItemEntity(itemDict, dim, (pos[0], pos[1] + 0.5, pos[2]))
|
||||
|
||||
# ============================================================================
|
||||
# 十二、工具方法:冷却检查
|
||||
# ============================================================================
|
||||
|
||||
def checkCooldown(self, key, cd=0.5):
|
||||
"""
|
||||
右键冷却:同一方块 cd 秒内只响应一次, 防止连点刷物品/刷提示。
|
||||
"""
|
||||
now = time.time()
|
||||
last = self.cooldown.get(key, 0)
|
||||
if now - last < cd:
|
||||
return False
|
||||
self.cooldown[key] = now
|
||||
return True
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
Reference in new issue
Block a user