完善客户端功能并同步差距文档

This commit is contained in:
shenlei
2026-09-03 18:40:01 +09:00
parent f93a172296
commit 13ccb8d02d
135 changed files with 21881 additions and 1259 deletions
+370 -171
View File
@@ -5,6 +5,7 @@
#include "classic/classic_session.h"
#include "game_client.h"
#include "mark_client.h"
#include "net_bounds.h"
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/os.hpp>
@@ -26,6 +27,27 @@ using namespace godot;
namespace mtgodot {
namespace {
bool valid_angle(double degrees) {
return std::isfinite(degrees);
}
bool valid_item_pos(int window, int cell) {
return mtnet::bounds::u8(window) && mtnet::bounds::u16(cell);
}
bool read_int(const Dictionary &dict, const char *key, int64_t &value, int64_t fallback) {
const Variant raw = dict.get(key, fallback);
if (raw.get_type() != Variant::INT) {
return false;
}
value = static_cast<int64_t>(raw);
return true;
}
} // namespace
M2Client::M2Client() {
set_process(true);
}
@@ -56,6 +78,7 @@ void M2Client::_bind_methods() {
ClassDB::bind_method(D_METHOD("attack", "motion", "victim_vid"), &M2Client::attack);
ClassDB::bind_method(D_METHOD("set_target", "victim_vid"), &M2Client::set_target);
ClassDB::bind_method(D_METHOD("say", "type", "text"), &M2Client::say);
ClassDB::bind_method(D_METHOD("send_emoticon", "index"), &M2Client::send_emoticon);
ClassDB::bind_method(D_METHOD("whisper", "to", "text"), &M2Client::whisper);
ClassDB::bind_method(D_METHOD("cast_skill", "motion_idx", "rot_deg", "x", "y"), &M2Client::cast_skill);
ClassDB::bind_method(D_METHOD("use_skill", "skill_id", "target_vid"), &M2Client::use_skill);
@@ -134,6 +157,7 @@ void M2Client::_bind_methods() {
PropertyInfo(Variant::DICTIONARY, "entity")));
ADD_SIGNAL(MethodInfo("chat", PropertyInfo(Variant::INT, "type"),
PropertyInfo(Variant::INT, "vid"), PropertyInfo(Variant::STRING, "text")));
ADD_SIGNAL(MethodInfo("emoticon_requested", PropertyInfo(Variant::INT, "index")));
ADD_SIGNAL(MethodInfo("whisper_received", PropertyInfo(Variant::INT, "sub"),
PropertyInfo(Variant::STRING, "from"), PropertyInfo(Variant::STRING, "text")));
// combat / status
@@ -449,6 +473,12 @@ void M2Client::net_poll() {
void M2Client::connect_to_server(const String &auth_host, int auth_port, const String &g_host,
int g_port, const String &id, const String &pw) {
disconnect_from_server();
if (auth_host.is_empty() || g_host.is_empty() || auth_port <= 0 || g_port <= 0 ||
!mtnet::bounds::u16(auth_port) || !mtnet::bounds::u16(g_port)) {
emit_signal("login_failed", String("invalid server address"));
set_stage(Stage::Failed);
return;
}
game_host = g_host;
game_port = g_port;
account_id = id;
@@ -466,6 +496,19 @@ void M2Client::connect_to_server(const String &auth_host, int auth_port, const S
// then connect to game_host with the returned login ticket.
if (OS::get_singleton()->get_environment("MT_PROTOCOL") == "classic") {
classic_sess = std::make_unique<mtnet::classic::ClassicSession>();
// §1.5 (W1 G1 wiring): CG_CLIENT_VERSION2 reports the running binary's
// filename; feed it the real executable name rather than the built-in
// default so the server-side version gate sees what ClientVS22 sends.
classic_sess->set_executable_name(
std::string(OS::get_singleton()->get_executable_path().get_file().utf8().get_data()));
// §2.2 (W1 G1 wiring): non-PC spawn names come from root/npclist.txt.
// M2Client has no asset-root handle, so the parser falls back to
// $MT_ASSETS/$M2_ASSETS (see npc_names.h default_npclist_path()); an
// explicit set_npclist_path() awaits asset-root plumbing into M2Client.
if (const String assets = OS::get_singleton()->get_environment("MT_ASSETS"); !assets.is_empty()) {
classic_sess->parser().set_npclist_path(
std::string(assets.utf8().get_data()) + "/root/npclist.txt");
}
// The 40250 server expects CG_ENTERGAME shortly after the loading phase
// starts. Keep the live client in the same timing window as m2dev while
// allowing a deployment-specific override for unusually large bursts.
@@ -570,14 +613,23 @@ bool M2Client::enter_game(int index) {
bool M2Client::create_character(int slot, const godot::String &name, int job, int shape,
int con, int intel, int str, int dex) {
const std::string name_wire = std::string(name.utf8().get_data());
const size_t name_limit = classic_sess ? mtnet::classic::CHARACTER_NAME_MAX_LEN :
mtnet::CHARACTER_NAME_MAX_LEN;
if (slot < 0 || slot >= mtnet::PLAYER_PER_ACCOUNT4 || name_wire.empty() ||
name_wire.size() > name_limit || !mtnet::bounds::u16(job) || !mtnet::bounds::u8(shape) ||
!mtnet::bounds::u8(con) || !mtnet::bounds::u8(intel) || !mtnet::bounds::u8(str) ||
!mtnet::bounds::u8(dex)) {
return false;
}
if (classic_sess) {
return classic_sess->create_character(slot, std::string(name.utf8().get_data()), job, shape,
return classic_sess->create_character(slot, name_wire, job, shape,
con, intel, str, dex);
}
if (!game) {
return false;
}
return game->create_character(slot, std::string(name.utf8().get_data()), job, shape,
return game->create_character(slot, name_wire, job, shape,
con, intel, str, dex);
}
@@ -682,7 +734,8 @@ godot::Array M2Client::build_char_list() const {
// --- in-game intents ---
bool M2Client::move(int func, int arg, double rot_deg, int x, int y) {
if (!is_in_game()) {
if (!is_in_game() || !mtnet::bounds::u8(func) || !mtnet::bounds::u8(arg) ||
!valid_angle(rot_deg) || !mtnet::bounds::i32(x) || !mtnet::bounds::i32(y)) {
return false;
}
double r = std::fmod(rot_deg, 360.0);
@@ -719,7 +772,7 @@ bool M2Client::request_warp() {
}
bool M2Client::fishing(double rot_deg) {
if (!is_in_game()) {
if (!is_in_game() || !valid_angle(rot_deg)) {
return false;
}
double r = std::fmod(rot_deg, 360.0);
@@ -757,11 +810,17 @@ bool M2Client::sync_positions(const Array &positions) {
if (!d.has("vid") || !d.has("x") || !d.has("y")) {
return false;
}
const int64_t vid = (int64_t)d["vid"];
const int64_t x = (int64_t)d["x"];
const int64_t y = (int64_t)d["y"];
if (vid < 0 || vid > 0xffffffffLL || x < INT32_MIN || x > INT32_MAX ||
y < INT32_MIN || y > INT32_MAX) {
const Variant raw_vid = d["vid"];
const Variant raw_x = d["x"];
const Variant raw_y = d["y"];
if (raw_vid.get_type() != Variant::INT || raw_x.get_type() != Variant::INT ||
raw_y.get_type() != Variant::INT) {
return false;
}
const int64_t vid = (int64_t)raw_vid;
const int64_t x = (int64_t)raw_x;
const int64_t y = (int64_t)raw_y;
if (!mtnet::bounds::u32(vid) || !mtnet::bounds::i32(x) || !mtnet::bounds::i32(y)) {
return false;
}
if (classic_sess) classic_elements.push_back({(uint32_t)vid, (int32_t)x, (int32_t)y});
@@ -773,7 +832,8 @@ bool M2Client::sync_positions(const Array &positions) {
}
bool M2Client::attack(int motion, int victim_vid) {
if (!is_in_game()) {
if (!is_in_game() || !mtnet::bounds::u8(motion) || victim_vid <= 0 ||
!mtnet::bounds::u32(victim_vid)) {
return false;
}
if (classic_sess) {
@@ -783,7 +843,8 @@ bool M2Client::attack(int motion, int victim_vid) {
}
bool M2Client::set_target(int victim_vid) {
if (!is_in_game()) {
// VID 0 is the stock client's explicit "clear target" packet.
if (!is_in_game() || victim_vid < 0 || !mtnet::bounds::u32(victim_vid)) {
return false;
}
if (classic_sess) {
@@ -793,34 +854,65 @@ bool M2Client::set_target(int victim_vid) {
}
bool M2Client::say(int type, const String &text) {
if (!is_in_game()) {
if (!is_in_game() || !mtnet::bounds::u8(type)) {
return false;
}
const std::string text_wire = std::string(text.utf8().get_data());
if ((classic_sess && text_wire.size() >= 512) ||
(!classic_sess && text_wire.size() > 0xffffu - sizeof(mtnet::CGChat) - 1)) {
return false;
}
if (classic_sess) {
return classic_sess->send_chat((uint8_t)type, std::string(text.utf8().get_data()));
return classic_sess->send_chat((uint8_t)type, text_wire);
}
return game && game->send_chat((uint8_t)type, std::string(text.utf8().get_data()));
return game && game->send_chat((uint8_t)type, text_wire);
}
bool M2Client::send_emoticon(int index) {
// Registration order from playersettingmodule.py. The reference client's
// SendEmoticon sends this token through SendChatPacket, while the received
// token is parsed back into the matching visual effect by the chat handler.
static constexpr const char *kTokens[] = {
"(sweat)", "(money)", "(happy)", "(like)", "(love)", "(angry)",
"(aha)", "(sad)", "(sorry)", "(!)", "(?)", "(fish)"
};
constexpr int kTokenCount = sizeof(kTokens) / sizeof(kTokens[0]);
if (index < 0 || index >= kTokenCount || !say(0 /* CHAT_TYPE_TALKING */, String(kTokens[index]))) {
return false;
}
emit_signal("emoticon_requested", index);
return true;
}
bool M2Client::whisper(const String &to, const String &text) {
const std::string to_wire = std::string(to.utf8().get_data());
const std::string text_wire = std::string(text.utf8().get_data());
if (classic_sess) {
return is_in_game() && classic_sess->send_whisper(std::string(to.utf8().get_data()),
std::string(text.utf8().get_data()));
return is_in_game() && !to_wire.empty() &&
to_wire.size() <= mtnet::classic::CHARACTER_NAME_MAX_LEN && text_wire.size() < 512 &&
classic_sess->send_whisper(to_wire, text_wire);
}
if (!game || !is_in_game()) {
if (!game || !is_in_game() || to_wire.empty() ||
to_wire.size() > mtnet::CHARACTER_NAME_MAX_LEN ||
text_wire.size() > 0xffffu - sizeof(mtnet::CGWhisper) - 1) {
return false;
}
return game->send_whisper(std::string(to.utf8().get_data()),
std::string(text.utf8().get_data()));
return game->send_whisper(to_wire, text_wire);
}
bool M2Client::cast_skill(int motion_idx, double rot_deg, int x, int y) {
// FUNC_SKILL | (motion_idx & 0x7F); reuse move()'s rot/time handling.
return move(0x80 | (motion_idx & 0x7F), 0, rot_deg, x, y);
// FUNC_SKILL | motion_idx; do not silently mask an invalid caller value.
if (motion_idx < 0 || motion_idx > 0x7f) {
return false;
}
return move(0x80 | motion_idx, 0, rot_deg, x, y);
}
bool M2Client::use_skill(int skill_id, int target_vid) {
if (!is_in_game() || skill_id <= 0) {
// §3.8 mod 3: signed params, range-checked before the uint32 wire cast.
// skill_id is a real skill index (0 < id < SKILL_MAX_NUM), not a free u32.
if (!is_in_game() || !mtnet::bounds::skill_index(skill_id) || target_vid < 0 ||
!mtnet::bounds::u32(target_vid)) {
return false;
}
if (classic_sess) {
@@ -857,7 +949,8 @@ bool M2Client::shoot(int skill_id) {
}
bool M2Client::add_fly_targeting(int target_vid, int x, int y) {
if (!is_in_game() || target_vid < 0) {
if (!is_in_game() || target_vid < 0 || !mtnet::bounds::u32(target_vid) ||
!mtnet::bounds::i32(x) || !mtnet::bounds::i32(y)) {
return false;
}
if (classic_sess) return classic_sess->send_add_fly_targeting((uint32_t)target_vid, (int32_t)x, (int32_t)y);
@@ -866,6 +959,10 @@ bool M2Client::add_fly_targeting(int target_vid, int x, int y) {
}
bool M2Client::skill_up(int skill_id) {
// §3.9 / §3.10: /skillup 只发正的、低于 SKILL_MAX_NUM 的技能索引(与 use_skill 同一谓词)。
if (!is_in_game() || !mtnet::bounds::skill_index(skill_id)) {
return false;
}
return say(0 /*CHAT_TYPE_TALKING*/, String("/skillup ") + String::num_int64(skill_id));
}
@@ -906,7 +1003,7 @@ Array M2Client::get_quickslots() const {
}
Dictionary d;
d["pos"] = pos;
d["type"] = (int)qs.type; // 1 item / 2 skill / 3 command / 4 emotion
d["type"] = (int)qs.type; // 1 inventory / 2 skill / 3 emotion / 4 shop (not executable)
d["ref"] = (int)qs.position; // item cell / skill id / ...
out.push_back(d);
}
@@ -914,8 +1011,8 @@ Array M2Client::get_quickslots() const {
}
bool M2Client::quickslot_add(int pos, int type, int ref) {
if (!is_in_game() || pos < 0 || pos >= mtnet::QUICKSLOT_MAX_NUM ||
type < 1 || type > 4 || ref < 0 || ref > 255) {
if (!is_in_game() || !mtnet::bounds::quick_slot(pos) || !mtnet::bounds::u8(type) ||
type < 1 || type > 3 || !mtnet::bounds::u8(ref)) {
return false;
}
if (classic_sess) return classic_sess->send_quickslot_add((uint8_t)pos, (uint8_t)type, (uint8_t)ref);
@@ -924,7 +1021,7 @@ bool M2Client::quickslot_add(int pos, int type, int ref) {
}
bool M2Client::quickslot_del(int pos) {
if (!is_in_game() || pos < 0 || pos >= mtnet::QUICKSLOT_MAX_NUM) {
if (!is_in_game() || !mtnet::bounds::quick_slot(pos)) {
return false;
}
if (classic_sess) return classic_sess->send_quickslot_del((uint8_t)pos);
@@ -933,8 +1030,8 @@ bool M2Client::quickslot_del(int pos) {
}
bool M2Client::quickslot_swap(int pos, int change_pos) {
if (!is_in_game() || pos < 0 || pos >= mtnet::QUICKSLOT_MAX_NUM ||
change_pos < 0 || change_pos >= mtnet::QUICKSLOT_MAX_NUM) {
if (!is_in_game() || !mtnet::bounds::quick_slot(pos) ||
!mtnet::bounds::quick_slot(change_pos)) {
return false;
}
if (classic_sess) return classic_sess->send_quickslot_swap((uint8_t)pos, (uint8_t)change_pos);
@@ -943,7 +1040,7 @@ bool M2Client::quickslot_swap(int pos, int change_pos) {
}
bool M2Client::click_npc(int vid) {
if (!is_in_game()) {
if (!is_in_game() || vid <= 0 || !mtnet::bounds::u32(vid)) {
return false;
}
if (classic_sess) {
@@ -952,7 +1049,7 @@ bool M2Client::click_npc(int vid) {
return game && game->send_on_click((uint32_t)vid);
}
bool M2Client::script_answer(int answer) {
if (!is_in_game()) {
if (!is_in_game() || !mtnet::bounds::u8(answer)) {
return false;
}
if (classic_sess) {
@@ -961,11 +1058,14 @@ bool M2Client::script_answer(int answer) {
return game && game->send_script_answer((uint8_t)answer);
}
bool M2Client::script_button(int idx) {
if (classic_sess) return is_in_game() && idx >= 0 && classic_sess->send_script_button((uint32_t)idx);
return game && is_in_game() && game->send_script_button((uint32_t)idx);
if (!is_in_game() || idx < 0 || !mtnet::bounds::u32(idx)) {
return false;
}
if (classic_sess) return classic_sess->send_script_button((uint32_t)idx);
return game && game->send_script_button((uint32_t)idx);
}
bool M2Client::script_select_item(int selection) {
if (!is_in_game() || selection < 0) {
if (!is_in_game() || selection < 0 || !mtnet::bounds::u32(selection)) {
return false;
}
if (classic_sess) return classic_sess->send_script_select_item((uint32_t)selection);
@@ -977,9 +1077,9 @@ bool M2Client::quest_input(const String &text) {
return game && is_in_game() && game->send_quest_input(std::string(text.utf8().get_data()));
}
bool M2Client::quest_confirm(bool yes, int request_pid) {
if (classic_sess) return is_in_game() && request_pid >= 0 &&
if (classic_sess) return is_in_game() && request_pid >= 0 && mtnet::bounds::u32(request_pid) &&
classic_sess->send_quest_confirm(yes, (uint32_t)request_pid);
return game && is_in_game() && request_pid >= 0 &&
return game && is_in_game() && request_pid >= 0 && mtnet::bounds::u32(request_pid) &&
game->send_quest_confirm(yes, (uint32_t)request_pid);
}
bool M2Client::quest_cancel() {
@@ -1015,20 +1115,26 @@ Array M2Client::get_quests() const {
// --- P8 social / shop / storage ---
bool M2Client::party_invite(int vid) {
if (classic_sess) return is_in_game() && vid > 0 && classic_sess->send_party_invite((uint32_t)vid);
return game && is_in_game() && game->send_party_invite((uint32_t)vid);
if (classic_sess) return is_in_game() && vid > 0 && mtnet::bounds::u32(vid) &&
classic_sess->send_party_invite((uint32_t)vid);
return game && is_in_game() && vid > 0 && mtnet::bounds::u32(vid) &&
game->send_party_invite((uint32_t)vid);
}
bool M2Client::party_answer(int leader_pid, bool accept) {
if (classic_sess) return is_in_game() && leader_pid > 0 &&
if (classic_sess) return is_in_game() && leader_pid > 0 && mtnet::bounds::u32(leader_pid) &&
classic_sess->send_party_answer((uint32_t)leader_pid, accept);
return game && is_in_game() && game->send_party_invite_answer((uint32_t)leader_pid, accept);
return game && is_in_game() && leader_pid > 0 && mtnet::bounds::u32(leader_pid) &&
game->send_party_invite_answer((uint32_t)leader_pid, accept);
}
bool M2Client::party_leave(int pid) {
if (classic_sess) return is_in_game() && pid > 0 && classic_sess->send_party_remove((uint32_t)pid);
return game && is_in_game() && pid >= 0 && game->send_party_remove((uint32_t)pid);
if (classic_sess) return is_in_game() && pid > 0 && mtnet::bounds::u32(pid) &&
classic_sess->send_party_remove((uint32_t)pid);
return game && is_in_game() && pid > 0 && mtnet::bounds::u32(pid) &&
game->send_party_remove((uint32_t)pid);
}
bool M2Client::party_use_skill(int skill_index, int target_vid) {
if (!is_in_game() || skill_index < 0 || skill_index > 255 || target_vid < 0) {
if (!is_in_game() || !mtnet::bounds::u8(skill_index) || target_vid < 0 ||
!mtnet::bounds::u32(target_vid)) {
return false;
}
if (classic_sess) return classic_sess->send_party_use_skill((uint8_t)skill_index, (uint32_t)target_vid);
@@ -1036,12 +1142,15 @@ bool M2Client::party_use_skill(int skill_index, int target_vid) {
return game->send_party_use_skill((uint8_t)skill_index, (uint32_t)target_vid);
}
bool M2Client::party_set_distribute(int mode) {
if (classic_sess) return is_in_game() && mode >= 0 && mode <= 255 &&
if (classic_sess) return is_in_game() && mtnet::bounds::u8(mode) &&
classic_sess->send_party_parameter((uint8_t)mode);
return game && is_in_game() && game->send_party_parameter((uint8_t)mode);
return game && is_in_game() && mtnet::bounds::u8(mode) &&
game->send_party_parameter((uint8_t)mode);
}
bool M2Client::party_set_state(int pid, int role, bool on) {
if (!is_in_game() || pid <= 0 || role < 0 || role > 255) return false;
if (!is_in_game() || pid <= 0 || !mtnet::bounds::u32(pid) || role < 0 || role >= 8) {
return false;
}
if (classic_sess) return classic_sess->send_party_set_state((uint32_t)pid, (uint8_t)role, on);
return game && game->send_party_set_state((uint32_t)pid, (uint8_t)role, on ? 1 : 0);
}
@@ -1130,15 +1239,18 @@ Dictionary M2Client::get_lover() const {
}
bool M2Client::shop_buy(int pos, int count) {
if (classic_sess) return is_in_game() && pos >= 0 && pos <= 255 &&
classic_sess->send_shop_buy((uint8_t)pos, (uint8_t)(count < 1 ? 1 : count));
return game && is_in_game() && game->send_shop_buy((uint8_t)pos, (uint8_t)(count < 1 ? 1 : count));
if (!is_in_game() || !mtnet::bounds::u8(pos) || count < 1 || !mtnet::bounds::u8(count)) {
return false;
}
if (classic_sess) return classic_sess->send_shop_buy((uint8_t)pos, (uint8_t)count);
return game && game->send_shop_buy((uint8_t)pos, (uint8_t)count);
}
bool M2Client::shop_sell(int inv_cell, int count) {
if (classic_sess) return is_in_game() && inv_cell >= 0 && inv_cell <= 255 &&
classic_sess->send_shop_sell((uint8_t)inv_cell, (uint8_t)(count < 1 ? 1 : count));
return game && is_in_game() &&
game->send_shop_sell((uint8_t)inv_cell, (uint8_t)(count < 1 ? 1 : count));
if (!is_in_game() || !mtnet::bounds::u8(inv_cell) || count < 0 || !mtnet::bounds::u8(count)) {
return false;
}
if (classic_sess) return classic_sess->send_shop_sell((uint8_t)inv_cell, (uint8_t)count);
return game->send_shop_sell((uint8_t)inv_cell, (uint8_t)count);
}
bool M2Client::shop_close() {
if (classic_sess) return is_in_game() && classic_sess->send_shop_close();
@@ -1206,27 +1318,34 @@ Dictionary M2Client::get_shop() const {
}
bool M2Client::exchange_start(int vid) {
if (classic_sess) {
return is_in_game() && classic_sess->send_exchange_start((uint32_t)vid);
}
return game && is_in_game() && game->send_exchange_start((uint32_t)vid);
}
bool M2Client::exchange_add_item(int inv_window, int inv_cell, int display_pos) {
if (classic_sess) {
return is_in_game() && classic_sess->send_exchange_item_add((uint8_t)inv_window,
(uint16_t)inv_cell, (uint8_t)display_pos);
}
return game && is_in_game() &&
game->send_exchange_item_add((uint8_t)inv_window, (uint16_t)inv_cell, (uint8_t)display_pos);
}
bool M2Client::exchange_add_gold(int gold) {
if (gold < 0) {
if (!is_in_game() || !mtnet::bounds::u32(vid) || vid <= 0) {
return false;
}
if (classic_sess) {
return is_in_game() && classic_sess->send_exchange_gold((uint32_t)gold);
return classic_sess->send_exchange_start((uint32_t)vid);
}
return game && is_in_game() && game->send_exchange_gold((uint32_t)gold);
return game && game->send_exchange_start((uint32_t)vid);
}
bool M2Client::exchange_add_item(int inv_window, int inv_cell, int display_pos) {
if (!is_in_game() || !valid_item_pos(inv_window, inv_cell) ||
!mtnet::bounds::exchange_slot(display_pos)) {
return false;
}
if (classic_sess) {
return classic_sess->send_exchange_item_add((uint8_t)inv_window, (uint16_t)inv_cell,
(uint8_t)display_pos);
}
return game && game->send_exchange_item_add((uint8_t)inv_window, (uint16_t)inv_cell,
(uint8_t)display_pos);
}
bool M2Client::exchange_add_gold(int gold) {
if (!is_in_game() || !mtnet::bounds::u32(gold)) {
return false;
}
if (classic_sess) {
return classic_sess->send_exchange_gold((uint32_t)gold);
}
return game && game->send_exchange_gold((uint32_t)gold);
}
bool M2Client::exchange_accept() {
if (classic_sess) {
@@ -1260,10 +1379,26 @@ Dictionary M2Client::get_exchange() const {
continue;
}
Dictionary s;
s["slot"] = i;
s["vnum"] = (int)arr[i].vnum;
s["count"] = (int)arr[i].count;
a.push_back(s);
s["slot"] = i;
s["vnum"] = (int)arr[i].vnum;
s["count"] = (int)arr[i].count;
Array sockets;
for (int j = 0; j < mtnet::ITEM_SOCKET_SLOT_MAX_NUM; ++j) {
sockets.push_back(arr[i].sockets[j]);
}
s["sockets"] = sockets;
Array attrs;
for (int j = 0; j < mtnet::ITEM_ATTRIBUTE_SLOT_MAX_NUM; ++j) {
if (arr[i].attrs[j].type == 0) {
continue;
}
Dictionary a;
a["type"] = (int)arr[i].attrs[j].type;
a["value"] = (int)arr[i].attrs[j].value;
attrs.push_back(a);
}
s["attrs"] = attrs;
a.push_back(s);
}
return a;
};
@@ -1273,28 +1408,37 @@ Dictionary M2Client::get_exchange() const {
}
bool M2Client::safebox_checkin(int safe_pos, int inv_window, int inv_cell) {
if (classic_sess) {
return is_in_game() && classic_sess->send_safebox_checkin((uint8_t)safe_pos,
(uint8_t)inv_window, (uint16_t)inv_cell);
if (!is_in_game() || !mtnet::bounds::u8(safe_pos) || !valid_item_pos(inv_window, inv_cell)) {
return false;
}
return game && is_in_game() &&
game->send_safebox_checkin((uint8_t)safe_pos, (uint8_t)inv_window, (uint16_t)inv_cell);
if (classic_sess) {
return classic_sess->send_safebox_checkin((uint8_t)safe_pos, (uint8_t)inv_window,
(uint16_t)inv_cell);
}
return game && game->send_safebox_checkin((uint8_t)safe_pos, (uint8_t)inv_window,
(uint16_t)inv_cell);
}
bool M2Client::safebox_checkout(int safe_pos, int inv_window, int inv_cell) {
if (classic_sess) {
return is_in_game() && classic_sess->send_safebox_checkout((uint8_t)safe_pos,
(uint8_t)inv_window, (uint16_t)inv_cell);
if (!is_in_game() || !mtnet::bounds::u8(safe_pos) || !valid_item_pos(inv_window, inv_cell)) {
return false;
}
return game && is_in_game() &&
game->send_safebox_checkout((uint8_t)safe_pos, (uint8_t)inv_window, (uint16_t)inv_cell);
if (classic_sess) {
return classic_sess->send_safebox_checkout((uint8_t)safe_pos, (uint8_t)inv_window,
(uint16_t)inv_cell);
}
return game && game->send_safebox_checkout((uint8_t)safe_pos, (uint8_t)inv_window,
(uint16_t)inv_cell);
}
bool M2Client::safebox_move(int from_cell, int to_cell, int count) {
if (classic_sess) {
return is_in_game() && classic_sess->send_safebox_move((uint16_t)from_cell,
(uint16_t)to_cell, (uint8_t)(count < 1 ? 1 : count));
if (!is_in_game() || !mtnet::bounds::u16(from_cell) || !mtnet::bounds::u16(to_cell) ||
!mtnet::bounds::u8(count)) {
return false;
}
return game && is_in_game() &&
game->send_safebox_move((uint16_t)from_cell, (uint16_t)to_cell, (uint8_t)(count < 1 ? 1 : count));
if (classic_sess) {
return classic_sess->send_safebox_move((uint16_t)from_cell, (uint16_t)to_cell,
(uint8_t)count);
}
return game && game->send_safebox_move((uint16_t)from_cell, (uint16_t)to_cell, (uint8_t)count);
}
bool M2Client::is_safebox_open() const {
const mtnet::EntityStore *world = active_world();
@@ -1358,9 +1502,12 @@ Array M2Client::get_mall_items() const {
return out;
}
bool M2Client::mall_checkout(int mall_pos, int inv_window, int inv_cell) {
if (!is_in_game() || !mtnet::bounds::u8(mall_pos) || !valid_item_pos(inv_window, inv_cell)) {
return false;
}
if (classic_sess) {
return is_in_game() && classic_sess->send_mall_checkout((uint8_t)mall_pos,
(uint8_t)inv_window, (uint16_t)inv_cell);
return classic_sess->send_mall_checkout((uint8_t)mall_pos, (uint8_t)inv_window,
(uint16_t)inv_cell);
}
return game && game->send_mall_checkout((uint8_t)mall_pos, (uint8_t)inv_window,
(uint16_t)inv_cell);
@@ -1383,38 +1530,76 @@ bool M2Client::mall_password(const String &password) {
// --- private (PC) shop ---
bool M2Client::open_private_shop(const godot::String &sign, const godot::Array &items) {
if (!game && !classic_sess) {
if (!is_in_game() || (!game && !classic_sess) ||
items.size() > mtnet::bounds::PRIVATE_SHOP_ITEM_COUNT) {
return false;
}
const std::string sign_wire = std::string(sign.utf8().get_data());
if (sign_wire.size() > mtnet::SHOP_SIGN_MAX_LEN) {
return false;
}
struct PrivateShopItemInput {
uint32_t vnum;
uint8_t count;
uint8_t inv_window;
uint16_t inv_cell;
uint32_t price;
uint8_t display_pos;
};
std::vector<PrivateShopItemInput> parsed;
parsed.reserve(items.size());
for (int i = 0; i < items.size(); ++i) {
const Variant raw = items[i];
if (raw.get_type() != Variant::DICTIONARY) {
return false;
}
const Dictionary d = raw;
int64_t vnum = 0;
int64_t count = 1;
int64_t inv_window = mtnet::WINDOW_INVENTORY;
int64_t inv_cell = 0;
int64_t price = 0;
int64_t display_pos = i;
if (!read_int(d, "vnum", vnum, 0) || !read_int(d, "count", count, 1) ||
!read_int(d, "inv_window", inv_window, mtnet::WINDOW_INVENTORY) ||
!read_int(d, "inv_cell", inv_cell, 0) || !read_int(d, "price", price, 0) ||
!read_int(d, "display_pos", display_pos, i) || !mtnet::bounds::u32(vnum) ||
!mtnet::bounds::u8(count) || count < 1 || !mtnet::bounds::u8(inv_window) ||
!mtnet::bounds::u16(inv_cell) || !mtnet::bounds::u32(price) ||
display_pos < 0 || !mtnet::bounds::shop_slot(display_pos)) {
return false;
}
parsed.push_back({(uint32_t)vnum, (uint8_t)count, (uint8_t)inv_window,
(uint16_t)inv_cell, (uint32_t)price, (uint8_t)display_pos});
}
if (classic_sess) {
std::vector<mtnet::classic::CGMyShopItem> v;
v.reserve(items.size());
for (int i = 0; i < items.size() && i < 39; ++i) {
Dictionary d = items[i];
v.reserve(parsed.size());
for (const auto &in : parsed) {
mtnet::classic::CGMyShopItem e{};
e.vnum = (uint32_t)(int64_t)d.get("vnum", 0);
e.count = (uint8_t)(int)d.get("count", 1);
e.pos = {(uint8_t)(int)d.get("inv_window", mtnet::WINDOW_INVENTORY),
(uint16_t)(int)d.get("inv_cell", 0)};
e.price = (uint32_t)(int64_t)d.get("price", 0);
e.display_pos = (uint8_t)(int)d.get("display_pos", i);
e.vnum = in.vnum;
e.count = in.count;
e.pos = {in.inv_window, in.inv_cell};
e.price = in.price;
e.display_pos = in.display_pos;
v.push_back(e);
}
return classic_sess->send_private_shop(std::string(sign.utf8().get_data()), v);
return classic_sess->send_private_shop(sign_wire, v);
}
std::vector<mtnet::MyShopItem> v;
for (int i = 0; i < items.size(); ++i) {
Dictionary d = items[i];
v.reserve(parsed.size());
for (const auto &in : parsed) {
mtnet::MyShopItem e{};
e.vnum = (uint32_t)(int64_t)d.get("vnum", 0);
e.count = (uint8_t)(int)d.get("count", 1);
e.pos = {(uint8_t)(int)d.get("inv_window", mtnet::WINDOW_INVENTORY),
(uint16_t)(int)d.get("inv_cell", 0)};
e.price = (uint32_t)(int64_t)d.get("price", 0);
e.display_pos = (uint8_t)(int)d.get("display_pos", i);
e.vnum = in.vnum;
e.count = in.count;
e.pos = {in.inv_window, in.inv_cell};
e.price = in.price;
e.display_pos = in.display_pos;
v.push_back(e);
}
return game->send_open_private_shop(std::string(sign.utf8().get_data()), v);
return game->send_open_private_shop(sign_wire, v);
}
bool M2Client::close_private_shop() {
if (classic_sess) {
@@ -1474,15 +1659,25 @@ Dictionary M2Client::get_cube() const {
return d;
}
bool M2Client::cube_make(int result_index) {
if (classic_sess) return is_in_game() && classic_sess->send_cube_make(result_index);
if (!is_in_game() || result_index < 0 || !mtnet::bounds::u32(result_index)) {
return false;
}
if (classic_sess) return classic_sess->send_cube_make(result_index);
return game && game->send_cube_make(result_index);
}
bool M2Client::cube_request_result_list(int npc_vnum) {
if (classic_sess) return is_in_game() && classic_sess->send_cube_result_list(npc_vnum);
if (!is_in_game() || !mtnet::bounds::u32(npc_vnum)) {
return false;
}
if (classic_sess) return classic_sess->send_cube_result_list(npc_vnum);
return game && game->send_cube_result_list((uint32_t)npc_vnum);
}
bool M2Client::cube_request_materials(int start_index, int count) {
if (classic_sess) return is_in_game() && classic_sess->send_cube_materials(start_index, count);
if (!is_in_game() || !mtnet::bounds::u8(start_index) || count < 1 ||
!mtnet::bounds::u8(count)) {
return false;
}
if (classic_sess) return classic_sess->send_cube_materials(start_index, count);
return game && game->send_cube_material_info(start_index, count);
}
bool M2Client::cube_open() {
@@ -1498,20 +1693,19 @@ bool M2Client::cube_list() {
return game && is_in_game() && game->send_chat(mtnet::CHAT_TYPE_COMMAND, "/cube list");
}
bool M2Client::cube_add_item(int cube_index, int inventory_index) {
if (classic_sess) {
return is_in_game() && classic_sess->send_cube_add_item(cube_index, inventory_index);
}
if (!game || !is_in_game() || cube_index < 0 || cube_index >= 24 ||
inventory_index < 0 || inventory_index > 255) {
if (!is_in_game() || !mtnet::bounds::cube_slot(cube_index) ||
!mtnet::bounds::u8(inventory_index)) {
return false;
}
if (classic_sess) return classic_sess->send_cube_add_item(cube_index, inventory_index);
if (!game) return false;
return game->send_chat(mtnet::CHAT_TYPE_COMMAND, "/cube add " + std::to_string(cube_index) +
" " + std::to_string(inventory_index));
}
bool M2Client::cube_delete_item(int cube_index) {
if (classic_sess) return is_in_game() && classic_sess->send_cube_delete_item(cube_index);
return game && is_in_game() && cube_index >= 0 && cube_index < 24 &&
game->send_chat(mtnet::CHAT_TYPE_COMMAND, "/cube delete " + std::to_string(cube_index));
if (!is_in_game() || !mtnet::bounds::cube_slot(cube_index)) return false;
if (classic_sess) return classic_sess->send_cube_delete_item(cube_index);
return game && game->send_chat(mtnet::CHAT_TYPE_COMMAND, "/cube delete " + std::to_string(cube_index));
}
// --- guild / refine ---
@@ -1571,23 +1765,25 @@ Array M2Client::get_guild_grades() const {
return out;
}
bool M2Client::guild_add_member(int vid) {
if (classic_sess) return is_in_game() && vid > 0 &&
if (classic_sess) return is_in_game() && vid > 0 && mtnet::bounds::u32(vid) &&
classic_sess->send_guild_u32(mtnet::classic::GUILD_CG_ADD_MEMBER, (uint32_t)vid);
return game && is_in_game() && game->send_guild_sub_u32(mtnet::GUILD_CG_ADD_MEMBER, (uint32_t)vid);
return game && is_in_game() && vid > 0 && mtnet::bounds::u32(vid) &&
game->send_guild_sub_u32(mtnet::GUILD_CG_ADD_MEMBER, (uint32_t)vid);
}
bool M2Client::guild_remove_member(int pid) {
if (classic_sess) return is_in_game() && pid > 0 &&
if (classic_sess) return is_in_game() && pid > 0 && mtnet::bounds::u32(pid) &&
classic_sess->send_guild_u32(mtnet::classic::GUILD_CG_REMOVE_MEMBER, (uint32_t)pid);
return game && is_in_game() &&
return game && is_in_game() && pid > 0 && mtnet::bounds::u32(pid) &&
game->send_guild_sub_u32(mtnet::GUILD_CG_REMOVE_MEMBER, (uint32_t)pid);
}
bool M2Client::guild_offer(int amount) {
if (classic_sess) return is_in_game() && amount >= 0 &&
if (classic_sess) return is_in_game() && mtnet::bounds::u32(amount) &&
classic_sess->send_guild_u32(mtnet::classic::GUILD_CG_OFFER, (uint32_t)amount);
return game && is_in_game() && game->send_guild_sub_u32(mtnet::GUILD_CG_OFFER, (uint32_t)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_charge_gsp(int amount) {
if (amount < 0) return false;
if (!mtnet::bounds::i32(amount)) return false;
if (classic_sess) return is_in_game() &&
classic_sess->send_guild_i32(mtnet::classic::GUILD_CG_CHARGE_GSP, (int32_t)amount);
return game && is_in_game() &&
@@ -1602,21 +1798,21 @@ bool M2Client::guild_change_grade_name(int grade, const String &name) {
return game && is_in_game() && game->send_guild_grade_name((uint8_t)grade, value);
}
bool M2Client::guild_change_grade_authority(int grade, int authority) {
if (grade < 1 || grade >= 16 || authority < 0 || authority > 255) return false;
if (grade < 1 || grade >= 16 || !mtnet::bounds::u8(authority)) return false;
if (classic_sess) return is_in_game() &&
classic_sess->send_guild_grade_authority((uint8_t)grade, (uint8_t)authority);
return game && is_in_game() &&
game->send_guild_grade_authority((uint8_t)grade, (uint8_t)authority);
}
bool M2Client::guild_change_member_grade(int pid, int grade) {
if (pid <= 0 || grade < 1 || grade >= 16) return false;
if (pid <= 0 || !mtnet::bounds::u32(pid) || grade < 1 || grade >= 16) return false;
if (classic_sess) return is_in_game() &&
classic_sess->send_guild_member_grade((uint32_t)pid, (uint8_t)grade);
return game && is_in_game() &&
game->send_guild_member_grade((uint32_t)pid, (uint8_t)grade);
}
bool M2Client::guild_change_member_general(int pid, bool enabled) {
if (pid <= 0) return false;
if (pid <= 0 || !mtnet::bounds::u32(pid)) return false;
if (classic_sess) return is_in_game() &&
classic_sess->send_guild_member_general((uint32_t)pid, enabled);
return game && is_in_game() &&
@@ -1629,7 +1825,7 @@ bool M2Client::guild_post_comment(const String &text) {
return game && is_in_game() && game->send_guild_comment(comment);
}
bool M2Client::guild_delete_comment(int comment_id) {
if (comment_id <= 0) return false;
if (comment_id <= 0 || !mtnet::bounds::u32(comment_id)) return false;
if (classic_sess) return is_in_game() &&
classic_sess->send_guild_u32(mtnet::classic::GUILD_CG_DELETE_COMMENT,
(uint32_t)comment_id);
@@ -1642,7 +1838,7 @@ bool M2Client::guild_refresh_comments() {
return game && is_in_game() && game->send_guild_sub(mtnet::GUILD_CG_REFRESH_COMMENT);
}
bool M2Client::guild_answer_invite(int guild_id, bool accept) {
if (guild_id <= 0) return false;
if (guild_id <= 0 || !mtnet::bounds::u32(guild_id)) return false;
if (classic_sess) return is_in_game() &&
classic_sess->send_guild_invite_answer((uint32_t)guild_id, accept);
return game && is_in_game() && game->send_guild_invite_answer((uint32_t)guild_id, accept);
@@ -1726,10 +1922,11 @@ String M2Client::get_guild_name(int guild_id) const {
}
bool M2Client::use_guild_skill(int skill_vnum, int target_vid) {
if (classic_sess) return is_in_game() && skill_vnum >= 0 && target_vid >= 0 &&
classic_sess->send_guild_skill((uint32_t)skill_vnum, (uint32_t)target_vid);
return game && is_in_game() &&
game->send_guild_use_skill((uint32_t)skill_vnum, (uint32_t)target_vid);
if (!is_in_game() || !mtnet::bounds::u32(skill_vnum) || !mtnet::bounds::u32(target_vid)) {
return false;
}
if (classic_sess) return classic_sess->send_guild_skill((uint32_t)skill_vnum, (uint32_t)target_vid);
return game && game->send_guild_use_skill((uint32_t)skill_vnum, (uint32_t)target_vid);
}
bool M2Client::declare_guild_war(const String &guild_name) {
@@ -1739,7 +1936,7 @@ bool M2Client::declare_guild_war(const String &guild_name) {
// --- guild marks ---
bool M2Client::download_guild_marks(const String &host, int port) {
if ((!game && !classic_sess) || port <= 0 || port > 65535 || host.is_empty()) {
if ((!game && !classic_sess) || port <= 0 || !mtnet::bounds::u16(port) || host.is_empty()) {
return false;
}
mark_host = host;
@@ -1777,7 +1974,8 @@ bool M2Client::download_guild_marks(const String &host, int port) {
}
bool M2Client::download_guild_symbol(const String &host, int port, int guild_id) {
if ((!game && !classic_sess) || port <= 0 || port > 65535 || host.is_empty() || guild_id <= 0) {
if ((!game && !classic_sess) || port <= 0 || !mtnet::bounds::u16(port) || host.is_empty() ||
guild_id <= 0 || !mtnet::bounds::u32(guild_id)) {
return false;
}
symbol_guild_id = 0;
@@ -1838,7 +2036,8 @@ Dictionary M2Client::get_mark_server() const {
}
bool M2Client::upload_guild_mark(const String &host, int port, int guild_id, const Ref<Image> &img) {
if ((!game && !classic_sess) || port <= 0 || port > 65535 || host.is_empty() || img.is_null()) {
if ((!game && !classic_sess) || port <= 0 || !mtnet::bounds::u16(port) || host.is_empty() ||
guild_id <= 0 || !mtnet::bounds::u32(guild_id) || img.is_null()) {
return false;
}
Ref<Image> rgba = img;
@@ -1893,7 +2092,8 @@ bool M2Client::upload_guild_mark(const String &host, int port, int guild_id, con
bool M2Client::upload_guild_symbol(const String &host, int port, int guild_id,
const PackedByteArray &file_bytes) {
if ((!game && !classic_sess) || port <= 0 || port > 65535 || host.is_empty() || file_bytes.is_empty()) {
if ((!game && !classic_sess) || port <= 0 || !mtnet::bounds::u16(port) || host.is_empty() ||
guild_id <= 0 || !mtnet::bounds::u32(guild_id) || file_bytes.is_empty()) {
return false;
}
std::vector<uint8_t> bytes(file_bytes.ptr(), file_bytes.ptr() + file_bytes.size());
@@ -2059,9 +2259,11 @@ void M2Client::pump_classic_mark() {
}
}
bool M2Client::refine(int pos, int type) {
if (classic_sess) return is_in_game() && pos >= 0 && pos < mtnet::INVENTORY_MAX_NUM &&
type >= 0 && type <= 255 && classic_sess->send_refine((uint8_t)pos, (uint8_t)type);
return game && is_in_game() && game->send_refine((uint8_t)pos, (uint8_t)type);
if (!is_in_game() || pos < 0 || pos >= mtnet::INVENTORY_MAX_NUM || !mtnet::bounds::u8(type)) {
return false;
}
if (classic_sess) return classic_sess->send_refine((uint8_t)pos, (uint8_t)type);
return game && game->send_refine((uint8_t)pos, (uint8_t)type);
}
bool M2Client::ds_refine(int mode, const Array &cells) {
@@ -2077,7 +2279,7 @@ bool M2Client::ds_refine(int mode, const Array &cells) {
if (classic_sess) {
mtnet::classic::ItemPos grid[mtnet::classic::DS_REFINE_GRID_MAX_NUM] = {};
int n = (int)cells.size();
if (n > mtnet::classic::DS_REFINE_GRID_MAX_NUM) n = mtnet::classic::DS_REFINE_GRID_MAX_NUM;
if (n > mtnet::classic::DS_REFINE_GRID_MAX_NUM) return false;
for (int i = 0; i < n; ++i) {
int cell = (int)cells[i];
if (cell < 0 || cell > 0xffff) return false;
@@ -2088,11 +2290,10 @@ bool M2Client::ds_refine(int mode, const Array &cells) {
if (!game) return false;
mtnet::ItemPos grid[mtnet::DS_REFINE_WINDOW_MAX_NUM] = {};
int n = (int)cells.size();
if (n > mtnet::DS_REFINE_WINDOW_MAX_NUM) {
n = mtnet::DS_REFINE_WINDOW_MAX_NUM;
}
if (n > mtnet::DS_REFINE_WINDOW_MAX_NUM) return false;
for (int i = 0; i < n; ++i) {
int cell = (int)cells[i];
if (cell < 0 || cell > 0xffff) return false;
grid[i].window_type = mtnet::WINDOW_INVENTORY;
grid[i].cell = (uint16_t)cell;
}
@@ -2246,6 +2447,7 @@ static Dictionary entity_dict(const mtnet::Entity &e) {
d["level"] = e.level;
d["dead"] = e.dead;
d["stunned"] = e.stunned;
d["knock_down"] = e.knock_down;
d["mount_vnum"] = (int)e.mount_vnum;
d["guild"] = e.guild;
d["alignment"] = (int)e.alignment;
@@ -2324,6 +2526,11 @@ Dictionary M2Client::get_points() const {
d["next_exp"] = (int64_t)(uint32_t)p.next_exp();
d["gold"] = p.gold();
d["attack_speed"] = p.v[mtnet::POINT_ATT_SPEED];
d["bow_distance"] = p.v[mtnet::POINT_BOW_DISTANCE]; // __GetBowRange() 距离加成
// §3.10 加号按钮门控:三个技能点来源分开取(uicharacter.py skillPageStatDict)。
d["skill_active"] = p.v[mtnet::POINT_SKILL];
d["skill_support"] = p.v[mtnet::POINT_SUB_SKILL];
d["skill_horse"] = p.v[mtnet::POINT_HORSE_SKILL];
d["energy"] = p.energy();
d["energy_end_time"] = p.energy_end_time();
Array values;
@@ -2370,7 +2577,8 @@ Array M2Client::get_affects() const {
// --- items ---
bool M2Client::move_item(int fw, int fc, int tw, int tc, int count) {
if (!is_in_game()) {
if (!is_in_game() || !valid_item_pos(fw, fc) || !valid_item_pos(tw, tc) ||
!mtnet::bounds::u8(count)) {
return false;
}
if (classic_sess) {
@@ -2381,7 +2589,7 @@ bool M2Client::move_item(int fw, int fc, int tw, int tc, int count) {
(uint8_t)count);
}
bool M2Client::use_item(int window, int cell) {
if (!is_in_game()) {
if (!is_in_game() || !valid_item_pos(window, cell)) {
return false;
}
if (classic_sess) {
@@ -2390,7 +2598,7 @@ bool M2Client::use_item(int window, int cell) {
return game && game->send_item_use((uint8_t)window, (uint16_t)cell);
}
bool M2Client::drop_item(int window, int cell, int gold) {
if (!is_in_game()) {
if (!is_in_game() || !valid_item_pos(window, cell) || !mtnet::bounds::u32(gold)) {
return false;
}
if (classic_sess) {
@@ -2399,7 +2607,8 @@ bool M2Client::drop_item(int window, int cell, int gold) {
return game && game->send_item_drop((uint8_t)window, (uint16_t)cell, (uint32_t)gold);
}
bool M2Client::drop_item_count(int window, int cell, int gold, int count) {
if (!is_in_game() || count < 1 || count > 255) return false;
if (!is_in_game() || !valid_item_pos(window, cell) || !mtnet::bounds::u32(gold) ||
count < 1 || !mtnet::bounds::u8(count)) return false;
if (classic_sess) {
return classic_sess->send_item_drop((uint8_t)window, (uint16_t)cell, (uint32_t)gold,
(uint8_t)count);
@@ -2408,19 +2617,17 @@ bool M2Client::drop_item_count(int window, int cell, int gold, int count) {
(uint8_t)count);
}
bool M2Client::use_item_to_item(int sw, int sc, int tw, int tc) {
if (!is_in_game()) return false;
if (!is_in_game() || !valid_item_pos(sw, sc) || !valid_item_pos(tw, tc)) return false;
if (classic_sess) {
if (sw < 0 || sw > 255 || tw < 0 || tw > 255 || sc < 0 || sc > 0xffff || tc < 0 || tc > 0xffff)
return false;
return classic_sess->send_item_use_to_item((uint8_t)sw, (uint16_t)sc, (uint8_t)tw, (uint16_t)tc);
}
if (!game) return false;
return game->send_item_use_to_item((uint8_t)sw, (uint16_t)sc, (uint8_t)tw, (uint16_t)tc);
}
bool M2Client::give_item(int target_vid, int window, int cell, int count) {
if (!is_in_game() || target_vid <= 0 || count < 1 || count > 255) return false;
if (!is_in_game() || target_vid <= 0 || !mtnet::bounds::u32(target_vid) ||
!valid_item_pos(window, cell) || count < 1 || !mtnet::bounds::u8(count)) return false;
if (classic_sess) {
if (window < 0 || window > 255 || cell < 0 || cell > 0xffff) return false;
return classic_sess->send_item_give((uint32_t)target_vid, (uint8_t)window, (uint16_t)cell,
(uint8_t)count);
}
@@ -2428,7 +2635,7 @@ bool M2Client::give_item(int target_vid, int window, int cell, int count) {
return game->send_give_item((uint32_t)target_vid, (uint8_t)window, (uint16_t)cell, (uint8_t)count);
}
bool M2Client::pickup_item(int ground_vid) {
if (!is_in_game()) {
if (!is_in_game() || ground_vid <= 0 || !mtnet::bounds::u32(ground_vid)) {
return false;
}
if (classic_sess) {
@@ -2604,18 +2811,10 @@ void M2Client::_process(double) {
}
}
void M2Client::_notification(int what) {
switch (what) {
case NOTIFICATION_APPLICATION_PAUSED:
suspend();
break;
case NOTIFICATION_APPLICATION_RESUMED:
resume();
break;
default:
break;
}
}
// §9.3: M2Client no longer listens for NOTIFICATION_APPLICATION_PAUSED/RESUMED.
// AppFlow owns the single AppLifecycle coordinator (equivalent to
// CPythonApplication); it drives suspend()/resume() below. Keeping a second
// listener here meant two entry points for the same lifecycle edge.
void M2Client::suspend() {
if (suspended) {