完善客户端功能并同步差距文档
This commit is contained in:
+370
-171
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user