装备校验链/快捷栏/公会/好友情侣/交易仓库五项差距修复(增量 129)
- 装备校验:can_equip 按 CanEquipNow→FindEquipCell→EquipItem 精确重排, 新增 find_equip_cell() 1:1 复刻(RING/UNIQUE 占用翻转、COSTUME/BELT/DS), 帝国 antiflag 降级为 tooltip 提示,LIMIT_PCBANG/REAL_TIME* 语义与倒计时文案 - 快捷栏:死亡全槽禁用+激活守卫、变身(affect 220)技能置灰、 物品槽失配置灰(服务器回收/换武器后刷新) - 公会:GUILD_AUTH_* 权限位门控踢人/公告/技能升级,/gskillup、 被宣战应答弹窗(/war·/nowar)、资金存取行(40250 服务端已禁用仍保留 1:1) - 好友/情侣:messenger 三固定组(好友/公会/情侣),lover_login/logout/ near/far/divorce 命令事件→在线/在身边状态与离婚隐藏 - 交易/仓库:ExchangeState.last_notice(ALREADY/LESS_GOLD)结束浮层提示; 仓库改密 /safebox_change_password 三段输入;关窗补 /safebox_close·/mall_close; 仓库金钱经核实 40250 无 wire 协议,UI 定界只读 - app_flow:重连时 guild_marks_ready 重复连接加 is_connected 守卫 - 相机 look_at 零向量守卫(首帧/传送落点) - 测试:equip/love/guild 断言更新,79 项中 78 绿(game_camera_test 为 HEAD 既有失败);真服冒烟 LIVE_SMOKE PASS(192.168.21.203 全链路) - docs/CLIENT-GAP.md 同步增量 129 Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018kUFvQGYusuY3dkfGitmAe
This commit is contained in:
@@ -718,7 +718,9 @@ bool ClassicParser::on_gc(uint8_t header, const uint8_t *body, uint32_t len) {
|
||||
break;
|
||||
case EXCHANGE_GC_ALREADY:
|
||||
case EXCHANGE_GC_LESS_GOLD:
|
||||
m_world.mut_exchange_notice();
|
||||
// 带上子头值(6 = 对方已在交易中 / 7 = 金钱不足),UI 据此给
|
||||
// 定点提示;成败的权威文案走 CHAT_TYPE_INFO 聊天通道。
|
||||
m_world.mut_exchange_notice(p.subheader);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -825,13 +827,20 @@ bool ClassicParser::on_gc(uint8_t header, const uint8_t *body, uint32_t len) {
|
||||
uint8_t flag = body[2];
|
||||
const uint8_t *cur = body + 3;
|
||||
const uint8_t *end = body + len;
|
||||
auto take_str = [&](size_t cap) -> std::string {
|
||||
// Every optional field is fixed-width on the wire (the server writes
|
||||
// the whole reserved buffer, NUL padding included), so advance by the
|
||||
// field width and not by the string length.
|
||||
auto take_str = [&](size_t width) -> std::string {
|
||||
if (static_cast<size_t>(end - cur) < width) {
|
||||
cur = end;
|
||||
return {};
|
||||
}
|
||||
size_t n = 0;
|
||||
while (cur + n < end && cur[n] != 0 && n < cap) {
|
||||
while (n < width && cur[n] != 0) {
|
||||
++n;
|
||||
}
|
||||
std::string s(reinterpret_cast<const char *>(cur), n);
|
||||
cur += (cur + n < end) ? n + 1 : n; // skip NUL if present
|
||||
cur += width;
|
||||
return s;
|
||||
};
|
||||
auto take_i32 = [&]() -> int32_t {
|
||||
@@ -844,25 +853,34 @@ bool ClassicParser::on_gc(uint8_t header, const uint8_t *body, uint32_t len) {
|
||||
};
|
||||
std::string title, clock_name, counter_name, icon;
|
||||
int32_t clock_value = 0, counter_value = 0;
|
||||
// QUEST_SEND_IS_BEGIN carries its own byte: set means "quest begins",
|
||||
// clear means "quest ends". Its presence alone is not the value.
|
||||
bool is_begin = false;
|
||||
if (flag & QUEST_SEND_IS_BEGIN) {
|
||||
if (cur < end) {
|
||||
is_begin = *cur != 0;
|
||||
++cur;
|
||||
}
|
||||
}
|
||||
if (flag & QUEST_SEND_TITLE) {
|
||||
title = take_str(28);
|
||||
title = take_str(QUEST_INFO_TITLE_SIZE);
|
||||
}
|
||||
if (flag & QUEST_SEND_CLOCK_NAME) {
|
||||
clock_name = take_str(16);
|
||||
clock_name = take_str(QUEST_INFO_CLOCK_NAME_SIZE);
|
||||
}
|
||||
if (flag & QUEST_SEND_CLOCK_VALUE) {
|
||||
clock_value = take_i32();
|
||||
}
|
||||
if (flag & QUEST_SEND_COUNTER_NAME) {
|
||||
counter_name = take_str(16);
|
||||
counter_name = take_str(QUEST_INFO_COUNTER_NAME_SIZE);
|
||||
}
|
||||
if (flag & QUEST_SEND_COUNTER_VALUE) {
|
||||
counter_value = take_i32();
|
||||
}
|
||||
if (flag & QUEST_SEND_ICON_FILE) {
|
||||
icon = take_str(24);
|
||||
icon = take_str(QUEST_INFO_ICON_FILE_SIZE);
|
||||
}
|
||||
m_world.mut_quest_info(index, flag, (flag & QUEST_SEND_IS_BEGIN) != 0, title, clock_name,
|
||||
m_world.mut_quest_info(index, flag, is_begin, title, clock_name,
|
||||
clock_value, counter_name, counter_value, icon);
|
||||
return true;
|
||||
}
|
||||
@@ -1497,15 +1515,6 @@ bool ClassicParser::on_gc(uint8_t header, const uint8_t *body, uint32_t len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
case HDR_GC_QUEST_SCRIPT:
|
||||
// Optional extended quest UI data. Consume it even when the UI is absent.
|
||||
return true;
|
||||
case HDR_GC_EXTENDED_STATUS:
|
||||
// Optional server status payload; framing is sufficient for connection safety.
|
||||
return true;
|
||||
case HDR_GC_EXTENDED_CHARACTER:
|
||||
// Optional extended character payload; consume without requiring its UI.
|
||||
return true;
|
||||
default:
|
||||
m_last_error = "phase " + m_phase_name + " does not handle GC header " +
|
||||
std::to_string(header);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <netdb.h>
|
||||
@@ -16,6 +17,27 @@
|
||||
namespace mtnet::classic {
|
||||
|
||||
namespace {
|
||||
// MT_NET_HEX=1 dumps the leading bytes of every framed packet next to the
|
||||
// existing wire trace. Framing bugs are only diagnosable from the actual
|
||||
// bytes, and the per-packet header/size line alone cannot show where a
|
||||
// mis-sized packet drags the stream out of alignment.
|
||||
bool hex_dump_enabled() {
|
||||
static const bool on = std::getenv("MT_NET_HEX") != nullptr;
|
||||
return on;
|
||||
}
|
||||
|
||||
void trace_hex(const char *tag, const char *name, uint8_t header, const uint8_t *p, size_t avail) {
|
||||
if (!hex_dump_enabled()) {
|
||||
return;
|
||||
}
|
||||
const size_t dump = avail < 24 ? avail : 24;
|
||||
std::fprintf(stderr, "[classic:%s] %s hdr=%u hex:", name, tag, header);
|
||||
for (size_t i = 0; i < dump; ++i) {
|
||||
std::fprintf(stderr, " %02x", p[i]);
|
||||
}
|
||||
std::fprintf(stderr, "\n");
|
||||
}
|
||||
|
||||
constexpr size_t RECV_CHUNK = 32 * 1024;
|
||||
|
||||
uint32_t now_ms() {
|
||||
@@ -486,14 +508,28 @@ void ClassicStream::dispatch() {
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
if (m_recv.readable() < dh.size) {
|
||||
uint32_t framed = dh.size;
|
||||
if (header == HDR_GC_QUEST_INFO) {
|
||||
// GC_QUEST_INFO always reports size == 6 on the wire; its real
|
||||
// length is flag-driven (see quest_info_packet_size).
|
||||
uint8_t head[QUEST_INFO_HEAD_SIZE];
|
||||
if (!m_recv.peek(head, sizeof(head))) {
|
||||
return;
|
||||
}
|
||||
framed = static_cast<uint32_t>(
|
||||
quest_info_packet_size(head[QUEST_INFO_HEAD_SIZE - 1]));
|
||||
}
|
||||
if (m_recv.readable() < framed) {
|
||||
return; // whole packet not here yet
|
||||
}
|
||||
if (m_trace) {
|
||||
trace_hex("recv", m_trace_name.c_str(), header, m_recv.read_ptr(), m_recv.readable());
|
||||
}
|
||||
m_recv.discard(sizeof(DynHead));
|
||||
uint32_t body_len = dh.size - sizeof(DynHead);
|
||||
uint32_t body_len = framed - sizeof(DynHead);
|
||||
const uint8_t *body = m_recv.read_ptr();
|
||||
if (m_trace) {
|
||||
std::fprintf(stderr, "[classic:%s] recv hdr=%u n=%u dynamic=1\n", m_trace_name.c_str(), header, dh.size);
|
||||
std::fprintf(stderr, "[classic:%s] recv hdr=%u n=%u dynamic=1\n", m_trace_name.c_str(), header, framed);
|
||||
}
|
||||
bool ok = !on_packet || on_packet(header, body, body_len);
|
||||
discard_recv(body_len);
|
||||
@@ -557,6 +593,7 @@ void ClassicStream::dispatch() {
|
||||
const uint8_t *p = m_recv.read_ptr();
|
||||
if (m_trace) {
|
||||
std::fprintf(stderr, "[classic:%s] recv hdr=%u n=%d dynamic=0\n", m_trace_name.c_str(), header, total);
|
||||
trace_hex("recv", m_trace_name.c_str(), header, p, m_recv.readable());
|
||||
if (header == HDR_GC_LOGIN_SUCCESS_NEWSLOT || header == HDR_GC_SKILL_COOLTIME_END) {
|
||||
const int next = m_recv.readable() > static_cast<size_t>(total) ? p[total] : -1;
|
||||
if (next < 0) {
|
||||
|
||||
@@ -276,12 +276,6 @@ enum : uint8_t {
|
||||
HDR_GC_EMPIRE = 90,
|
||||
HDR_GC_PARTY_LINK = 91,
|
||||
HDR_GC_PARTY_UNLINK = 92,
|
||||
// 40250 extended quest/script notification; payload is dynamically framed.
|
||||
HDR_GC_QUEST_SCRIPT = 148,
|
||||
// Extended servers also emit a dynamically framed mount/status notice here.
|
||||
HDR_GC_EXTENDED_STATUS = 59,
|
||||
// 40250 extended character/status packet (dynamic framing).
|
||||
HDR_GC_EXTENDED_CHARACTER = 105,
|
||||
HDR_GC_REFINE_INFORMATION_OLD = 95,
|
||||
HDR_GC_OBSERVER_ADD = 96,
|
||||
HDR_GC_OBSERVER_REMOVE = 97,
|
||||
@@ -1063,6 +1057,34 @@ enum : uint8_t {
|
||||
QUEST_SEND_COUNTER_VALUE = 1 << 5,
|
||||
QUEST_SEND_ICON_FILE = 1 << 6,
|
||||
};
|
||||
// Field widths written by PC::SendQuestInfoPakcet() (questpc.cpp) and read back
|
||||
// by the original client's field-by-field Recv() in RecvQuestInfoPacket().
|
||||
inline constexpr int QUEST_INFO_HEAD_SIZE = 6; // header + u16 size + u16 index + u8 flag
|
||||
inline constexpr int QUEST_INFO_IS_BEGIN_SIZE = 1;
|
||||
inline constexpr int QUEST_INFO_TITLE_SIZE = 30 + 1;
|
||||
inline constexpr int QUEST_INFO_CLOCK_NAME_SIZE = 16 + 1;
|
||||
inline constexpr int QUEST_INFO_CLOCK_VALUE_SIZE = 4;
|
||||
inline constexpr int QUEST_INFO_COUNTER_NAME_SIZE = 16 + 1;
|
||||
inline constexpr int QUEST_INFO_COUNTER_VALUE_SIZE = 4;
|
||||
inline constexpr int QUEST_INFO_ICON_FILE_SIZE = 24 + 1;
|
||||
|
||||
// The 40250 server fills the fixed head with `size = sizeof(packet_quest_info)`
|
||||
// (6) and then appends the optional fields while only bumping its own local
|
||||
// copy of `size` — the count on the wire never covers the tail. Framing this
|
||||
// packet off its size field therefore leaves the tail in the stream and
|
||||
// desyncs every packet after it, so derive the real length from `flag` the way
|
||||
// the original client does.
|
||||
constexpr int quest_info_packet_size(uint8_t flag) {
|
||||
int n = QUEST_INFO_HEAD_SIZE;
|
||||
if (flag & QUEST_SEND_IS_BEGIN) n += QUEST_INFO_IS_BEGIN_SIZE;
|
||||
if (flag & QUEST_SEND_TITLE) n += QUEST_INFO_TITLE_SIZE;
|
||||
if (flag & QUEST_SEND_CLOCK_NAME) n += QUEST_INFO_CLOCK_NAME_SIZE;
|
||||
if (flag & QUEST_SEND_CLOCK_VALUE) n += QUEST_INFO_CLOCK_VALUE_SIZE;
|
||||
if (flag & QUEST_SEND_COUNTER_NAME) n += QUEST_INFO_COUNTER_NAME_SIZE;
|
||||
if (flag & QUEST_SEND_COUNTER_VALUE) n += QUEST_INFO_COUNTER_VALUE_SIZE;
|
||||
if (flag & QUEST_SEND_ICON_FILE) n += QUEST_INFO_ICON_FILE_SIZE;
|
||||
return n;
|
||||
}
|
||||
|
||||
// --- GC party ---
|
||||
inline constexpr int PARTY_AFFECT_SLOT_MAX_NUM = 7;
|
||||
@@ -1465,9 +1487,6 @@ constexpr bool is_dynamic_gc(uint8_t h) {
|
||||
case HDR_GC_GUILD:
|
||||
case HDR_GC_MESSENGER:
|
||||
case HDR_GC_QUEST_INFO:
|
||||
case HDR_GC_QUEST_SCRIPT:
|
||||
case HDR_GC_EXTENDED_STATUS:
|
||||
case HDR_GC_EXTENDED_CHARACTER:
|
||||
case HDR_GC_DUEL_START:
|
||||
case HDR_GC_SYNC_POSITION:
|
||||
// client CMainPacketHeaderMap registers GC_WHISPER STATIC, but its
|
||||
|
||||
@@ -1083,6 +1083,26 @@ void EntityStore::mut_lover(const std::string &name, uint8_t love_point) {
|
||||
m_lover.name = name;
|
||||
m_lover.love_point = love_point;
|
||||
m_lover.valid = !name.empty();
|
||||
if (!m_lover.valid) {
|
||||
m_lover.online = false;
|
||||
m_lover.near = false;
|
||||
}
|
||||
m_lover_dirty = true;
|
||||
}
|
||||
|
||||
void EntityStore::mut_lover_online(bool online) {
|
||||
if (!m_lover.valid || m_lover.online == online) {
|
||||
return;
|
||||
}
|
||||
m_lover.online = online;
|
||||
m_lover_dirty = true;
|
||||
}
|
||||
|
||||
void EntityStore::mut_lover_near(bool near) {
|
||||
if (!m_lover.valid || m_lover.near == near) {
|
||||
return;
|
||||
}
|
||||
m_lover.near = near;
|
||||
m_lover_dirty = true;
|
||||
}
|
||||
|
||||
@@ -1369,7 +1389,7 @@ void EntityStore::mut_guild_invite(uint32_t guild_id, const std::string &guild_n
|
||||
}
|
||||
|
||||
void EntityStore::mut_exchange_start(uint32_t partner_vid) {
|
||||
m_exchange = ExchangeState{};
|
||||
m_exchange = ExchangeState{}; // last_notice 一并清零
|
||||
m_exchange.active = true;
|
||||
m_exchange.partner_vid = partner_vid;
|
||||
m_exchange_dirty = true;
|
||||
@@ -1415,7 +1435,8 @@ void EntityStore::mut_exchange_end() {
|
||||
m_exchange_dirty = true;
|
||||
}
|
||||
|
||||
void EntityStore::mut_exchange_notice() {
|
||||
void EntityStore::mut_exchange_notice(uint8_t code) {
|
||||
m_exchange.last_notice = code;
|
||||
m_exchange_dirty = true;
|
||||
}
|
||||
|
||||
@@ -2598,6 +2619,11 @@ void EntityStore::apply(uint16_t header, const void *body, uint16_t len) {
|
||||
case EXCHANGE_GC_END:
|
||||
x = ExchangeState{};
|
||||
break;
|
||||
case EXCHANGE_GC_ALREADY:
|
||||
case EXCHANGE_GC_LESS_ELK:
|
||||
// 与 classic 解析路径一致:只带这两个可机器判定的附加码。
|
||||
x.last_notice = (uint8_t)p.subheader;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -3319,6 +3345,32 @@ void EntityStore::apply_server_command(const std::string &line) {
|
||||
mut_friend_invite(tok[1]);
|
||||
return;
|
||||
}
|
||||
if (tok[0] == "lover_login") {
|
||||
mut_lover_online(true);
|
||||
m_server_commands.push_back({ServerCommandEvent::LoverLogin, 1, 0});
|
||||
return;
|
||||
}
|
||||
if (tok[0] == "lover_logout") {
|
||||
mut_lover_online(false);
|
||||
mut_lover_near(false);
|
||||
m_server_commands.push_back({ServerCommandEvent::LoverLogout, 0, 0});
|
||||
return;
|
||||
}
|
||||
if (tok[0] == "lover_near") {
|
||||
mut_lover_near(true);
|
||||
m_server_commands.push_back({ServerCommandEvent::LoverNear, 1, 0});
|
||||
return;
|
||||
}
|
||||
if (tok[0] == "lover_far") {
|
||||
mut_lover_near(false);
|
||||
m_server_commands.push_back({ServerCommandEvent::LoverFar, 0, 0});
|
||||
return;
|
||||
}
|
||||
if (tok[0] == "lover_divorce") {
|
||||
mut_lover("", 0);
|
||||
m_server_commands.push_back({ServerCommandEvent::LoverDivorce, 0, 0});
|
||||
return;
|
||||
}
|
||||
if (tok[0] == "gift") {
|
||||
m_server_commands.push_back({ServerCommandEvent::GiftAvailable, 0, 0});
|
||||
return;
|
||||
|
||||
@@ -309,6 +309,10 @@ struct ExchangeState {
|
||||
int64_t peer_gold = 0;
|
||||
bool self_accept = false;
|
||||
bool peer_accept = false;
|
||||
// EXCHANGE_GC_ALREADY / LESS_GOLD 的线上的子头值(0 = 无)。服务器在成功与
|
||||
// 取消两种结局都发 GC_END(exchange.cpp EXCHANGE_END 统一 Cancel),成败区分
|
||||
// 只靠 CHAT_TYPE_INFO 文案;这里只承载这两个机器可判的附加码。
|
||||
uint8_t last_notice = 0;
|
||||
};
|
||||
|
||||
// --- guild (GC_GUILD) --------------------------------------------------------
|
||||
@@ -414,6 +418,11 @@ struct ServerCommandEvent {
|
||||
MobileAuthRequired,
|
||||
ComboChanged,
|
||||
GiftAvailable,
|
||||
LoverLogin,
|
||||
LoverLogout,
|
||||
LoverNear,
|
||||
LoverFar,
|
||||
LoverDivorce,
|
||||
};
|
||||
Kind kind = PartyRequestDenied;
|
||||
uint32_t value = 0; // item vnum / price, depending on kind
|
||||
@@ -460,6 +469,8 @@ struct LoverInfo {
|
||||
std::string name;
|
||||
uint8_t love_point = 0;
|
||||
bool valid = false;
|
||||
bool online = false; // CHAT_TYPE_COMMAND "lover_login"/"lover_logout"(marriage.cpp)
|
||||
bool near = false; // CHAT_TYPE_COMMAND "lover_near"/"lover_far"
|
||||
};
|
||||
|
||||
// The local player's full stat array (GC_PLAYER_POINTS), indexed by EPointTypes.
|
||||
@@ -645,6 +656,8 @@ public:
|
||||
void mut_marker_delete(int32_t id);
|
||||
void mut_observer(ObserverEvent::Kind kind, uint32_t vid, int32_t x, int32_t y);
|
||||
void mut_lover(const std::string &name, uint8_t love_point);
|
||||
void mut_lover_online(bool online);
|
||||
void mut_lover_near(bool near);
|
||||
void mut_love_point(uint8_t love_point);
|
||||
void mut_fishing(uint8_t subheader, uint32_t info, uint8_t dir);
|
||||
void mut_dungeon(uint8_t subheader, int32_t x, int32_t y, bool has_destination);
|
||||
@@ -675,7 +688,7 @@ public:
|
||||
void mut_exchange_gold(bool self, int64_t gold);
|
||||
void mut_exchange_accept(bool self, bool accepted);
|
||||
void mut_exchange_end();
|
||||
void mut_exchange_notice(); // server-side ALREADY / LESS_GOLD notice
|
||||
void mut_exchange_notice(uint8_t code); // EXCHANGE_GC_ALREADY / LESS_GOLD 子头值
|
||||
void mut_safebox_open(int size);
|
||||
void mut_safebox_money(int64_t money);
|
||||
void mut_safebox_set(uint16_t cell, uint32_t vnum, uint8_t count, uint32_t flags,
|
||||
|
||||
@@ -245,6 +245,9 @@ void M2Client::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("get_safebox_gold"), &M2Client::get_safebox_gold);
|
||||
ClassDB::bind_method(D_METHOD("get_safebox_items"), &M2Client::get_safebox_items);
|
||||
ClassDB::bind_method(D_METHOD("safebox_password", "password"), &M2Client::safebox_password);
|
||||
ClassDB::bind_method(D_METHOD("safebox_change_password", "old_password", "new_password"),
|
||||
&M2Client::safebox_change_password);
|
||||
ClassDB::bind_method(D_METHOD("safebox_close"), &M2Client::safebox_close);
|
||||
// item-mall (창고몰)
|
||||
ClassDB::bind_method(D_METHOD("is_mall_open"), &M2Client::is_mall_open);
|
||||
ClassDB::bind_method(D_METHOD("get_mall_size"), &M2Client::get_mall_size);
|
||||
@@ -252,6 +255,7 @@ void M2Client::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("mall_checkout", "mall_pos", "inv_window", "inv_cell"),
|
||||
&M2Client::mall_checkout);
|
||||
ClassDB::bind_method(D_METHOD("mall_password", "password"), &M2Client::mall_password);
|
||||
ClassDB::bind_method(D_METHOD("mall_close"), &M2Client::mall_close);
|
||||
// private (PC) shop
|
||||
ClassDB::bind_method(D_METHOD("open_private_shop", "sign", "items"),
|
||||
&M2Client::open_private_shop);
|
||||
@@ -314,6 +318,8 @@ void M2Client::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("guild_remove_member", "pid"), &M2Client::guild_remove_member);
|
||||
ClassDB::bind_method(D_METHOD("guild_offer", "amount"), &M2Client::guild_offer);
|
||||
ClassDB::bind_method(D_METHOD("guild_charge_gsp", "amount"), &M2Client::guild_charge_gsp);
|
||||
ClassDB::bind_method(D_METHOD("guild_deposit_money", "amount"), &M2Client::guild_deposit_money);
|
||||
ClassDB::bind_method(D_METHOD("guild_withdraw_money", "amount"), &M2Client::guild_withdraw_money);
|
||||
ClassDB::bind_method(D_METHOD("guild_change_grade_name", "grade", "name"),
|
||||
&M2Client::guild_change_grade_name);
|
||||
ClassDB::bind_method(D_METHOD("guild_change_grade_authority", "grade", "authority"),
|
||||
@@ -1236,6 +1242,8 @@ Dictionary M2Client::get_lover() const {
|
||||
d["valid"] = lover.valid;
|
||||
d["name"] = String::utf8(lover.name.c_str());
|
||||
d["love_point"] = (int)lover.love_point;
|
||||
d["online"] = lover.online;
|
||||
d["near"] = lover.near;
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -1373,6 +1381,9 @@ Dictionary M2Client::get_exchange() const {
|
||||
d["peer_gold"] = (int64_t)x.peer_gold;
|
||||
d["self_accept"] = x.self_accept;
|
||||
d["peer_accept"] = x.peer_accept;
|
||||
// EXCHANGE_GC_ALREADY(6)/LESS_GOLD(7) 的子头值;0 = 无。成功/取消的区分
|
||||
// 只看 CHAT_TYPE_INFO 文案(服务器两种结局都发 GC_END)。
|
||||
d["notice"] = (int)x.last_notice;
|
||||
auto slots = [](const mtnet::ExchangeSlot *arr) {
|
||||
Array a;
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
@@ -1528,6 +1539,23 @@ bool M2Client::mall_password(const String &password) {
|
||||
return say(0, String("/mall_password ") + password);
|
||||
}
|
||||
|
||||
bool M2Client::safebox_change_password(const String &old_password, const String &new_password) {
|
||||
// 服务器 do_safebox_change_password 要求两个参数各 1..6 字符。
|
||||
if (old_password.is_empty() || old_password.length() > 6 ||
|
||||
new_password.is_empty() || new_password.length() > 6) {
|
||||
return false;
|
||||
}
|
||||
return say(0, String("/safebox_change_password ") + old_password + " " + new_password);
|
||||
}
|
||||
|
||||
bool M2Client::safebox_close() {
|
||||
return say(0, "/safebox_close");
|
||||
}
|
||||
|
||||
bool M2Client::mall_close() {
|
||||
return say(0, "/mall_close");
|
||||
}
|
||||
|
||||
// --- private (PC) shop ---
|
||||
|
||||
bool M2Client::open_private_shop(const godot::String &sign, const godot::Array &items) {
|
||||
@@ -1783,6 +1811,25 @@ bool M2Client::guild_offer(int amount) {
|
||||
return game && is_in_game() && mtnet::bounds::u32(amount) &&
|
||||
game->send_guild_sub_u32(mtnet::GUILD_CG_OFFER, (uint32_t)amount);
|
||||
}
|
||||
bool M2Client::guild_deposit_money(int amount) {
|
||||
// 40250 服务器禁用了存款处理(input_main.cpp GUILD_CG_DEPOSIT_MONEY 直接
|
||||
// return);原版 UI 仍发送该包,这里保持同样的线上行为。
|
||||
if (classic_sess) return is_in_game() && mtnet::bounds::u32(amount) &&
|
||||
classic_sess->send_guild_u32(mtnet::classic::GUILD_CG_DEPOSIT_MONEY,
|
||||
(uint32_t)amount);
|
||||
return game && is_in_game() && mtnet::bounds::u32(amount) &&
|
||||
game->send_guild_sub_u32(mtnet::GUILD_CG_DEPOSIT_MONEY, (uint32_t)amount);
|
||||
}
|
||||
|
||||
bool M2Client::guild_withdraw_money(int amount) {
|
||||
// 同上:40250 服务器禁用取款(且原逻辑仅会长可取,上限 500000)。
|
||||
if (classic_sess) return is_in_game() && mtnet::bounds::u32(amount) &&
|
||||
classic_sess->send_guild_u32(mtnet::classic::GUILD_CG_WITHDRAW_MONEY,
|
||||
(uint32_t)amount);
|
||||
return game && is_in_game() && mtnet::bounds::u32(amount) &&
|
||||
game->send_guild_sub_u32(mtnet::GUILD_CG_WITHDRAW_MONEY, (uint32_t)amount);
|
||||
}
|
||||
|
||||
bool M2Client::guild_charge_gsp(int amount) {
|
||||
if (!mtnet::bounds::i32(amount)) return false;
|
||||
if (classic_sess) return is_in_game() &&
|
||||
@@ -3170,6 +3217,14 @@ void M2Client::pump_classic() {
|
||||
case mtnet::ServerCommandEvent::PartyRequestDenied:
|
||||
emit_signal("party_request_denied");
|
||||
break;
|
||||
case mtnet::ServerCommandEvent::LoverLogin:
|
||||
case mtnet::ServerCommandEvent::LoverLogout:
|
||||
case mtnet::ServerCommandEvent::LoverNear:
|
||||
case mtnet::ServerCommandEvent::LoverFar:
|
||||
case mtnet::ServerCommandEvent::LoverDivorce:
|
||||
// LoverInfo 由 mutator 同步更新;lover_changed 信号在
|
||||
// take_lover_dirty() 处统一发出,这里无需单独 emit。
|
||||
break;
|
||||
case mtnet::ServerCommandEvent::SafeboxPasswordRequired:
|
||||
emit_signal("safebox_password_required");
|
||||
break;
|
||||
@@ -3680,6 +3735,14 @@ void M2Client::pump_game() {
|
||||
case mtnet::ServerCommandEvent::PartyRequestDenied:
|
||||
emit_signal("party_request_denied");
|
||||
break;
|
||||
case mtnet::ServerCommandEvent::LoverLogin:
|
||||
case mtnet::ServerCommandEvent::LoverLogout:
|
||||
case mtnet::ServerCommandEvent::LoverNear:
|
||||
case mtnet::ServerCommandEvent::LoverFar:
|
||||
case mtnet::ServerCommandEvent::LoverDivorce:
|
||||
// LoverInfo 由 mutator 同步更新;lover_changed 信号在
|
||||
// take_lover_dirty() 处统一发出,这里无需单独 emit。
|
||||
break;
|
||||
case mtnet::ServerCommandEvent::SafeboxPasswordRequired:
|
||||
emit_signal("safebox_password_required");
|
||||
break;
|
||||
|
||||
@@ -161,6 +161,11 @@ public:
|
||||
int get_safebox_gold() const;
|
||||
godot::Array get_safebox_items() const; // [{cell, vnum, count}]
|
||||
bool safebox_password(const godot::String &password);
|
||||
// 40250 的仓库关闭/改密走 chat 命令(cmd.cpp do_safebox_close /
|
||||
// do_safebox_change_password,均 GM_PLAYER),与 safebox_password 同通道。
|
||||
bool safebox_change_password(const godot::String &old_password,
|
||||
const godot::String &new_password);
|
||||
bool safebox_close();
|
||||
|
||||
// --- item-mall (창고몰) ---
|
||||
bool is_mall_open() const;
|
||||
@@ -168,6 +173,7 @@ public:
|
||||
godot::Array get_mall_items() const; // [{cell, vnum, count}]
|
||||
bool mall_checkout(int mall_pos, int inv_window, int inv_cell);
|
||||
bool mall_password(const godot::String &password);
|
||||
bool mall_close(); // "/mall_close"(cmd.cpp do_mall_close)
|
||||
|
||||
// --- private (PC) shop ---
|
||||
// items: Array of {vnum, count, inv_cell, price, display_pos}
|
||||
@@ -193,6 +199,10 @@ public:
|
||||
bool guild_remove_member(int pid);
|
||||
bool guild_offer(int amount);
|
||||
bool guild_charge_gsp(int amount);
|
||||
// 40250 服务器把这两个子头禁用了(input_main.cpp 直接 return,死代码在下方),
|
||||
// 但原版 ClientVS22 UI 仍照发(uiguild.py OnDeposit/OnWithdraw)——保持 1:1。
|
||||
bool guild_deposit_money(int amount);
|
||||
bool guild_withdraw_money(int amount);
|
||||
bool guild_change_grade_name(int grade, const godot::String &name);
|
||||
bool guild_change_grade_authority(int grade, int authority);
|
||||
bool guild_change_member_grade(int pid, int grade);
|
||||
|
||||
Reference in New Issue
Block a user