Batch 4-b: verify combat on the port route
The fake server spawns a stray dog outside the village's BANPK safe disc and answers CG_TARGET/CG_ATTACK with GC_TARGET/GC_DAMAGE_INFO/GC_DEAD. port.login_flow and python_game_render_test hold the left button on it (MBF_SMART -> __OnPressActor) until it dies. Adds the host/test helper PythonBoot::Evaluate / Metin2PythonHost.evaluate. python_3d_surface: emission is now multiplied by the texture, like D3D's emissive-then-MODULATE, which removes the grey click-glow quad and the grey wash on characters. Damage numbers wait on CPythonSystem (4-g). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6ea0fa9889
commit
85e176bdd5
@@ -554,6 +554,25 @@ bool RunLine(const char* source, std::string* error)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Evaluate(const char* expression, std::string* result, std::string* error)
|
||||
{
|
||||
// A host/test helper with no 40250 counterpart: RunLine stores str(expression) in __main__, read back here.
|
||||
if (!RunLine(("__mt_evaluate = str(" + std::string(expression ? expression : "None") + ")").c_str(), error))
|
||||
return false;
|
||||
PyObject* main = PyImport_AddModule("__main__");
|
||||
PyObject* value = main ? PyObject_GetAttrString(main, "__mt_evaluate") : nullptr;
|
||||
if (!value || !PyString_Check(value))
|
||||
{
|
||||
Py_XDECREF(value);
|
||||
PyErr_Clear();
|
||||
return fail(error, "evaluate: no result");
|
||||
}
|
||||
result->assign(PyString_AsString(value));
|
||||
Py_DECREF(value);
|
||||
PyObject_DelAttrString(main, "__mt_evaluate");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
if (!g_launcher)
|
||||
|
||||
@@ -48,6 +48,8 @@ void AppRequestQuit();
|
||||
// check uses it to prove the interpreter and the standard library work in-process on a device that has
|
||||
// no 40250 packs, which is where RunMainScript cannot run at all.
|
||||
bool RunLine(const char* source, std::string* error);
|
||||
// str(expression) evaluated in __main__ (a host/test helper; 40250 has no counterpart).
|
||||
bool Evaluate(const char* expression, std::string* result, std::string* error);
|
||||
|
||||
// Forward physical Godot input to the original 40250 window manager. Coordinates
|
||||
// are viewport pixels; SetUISize also establishes its logical resolution.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "python_host_node.h"
|
||||
|
||||
#include <godot_cpp/core/class_db.hpp>
|
||||
#include <godot_cpp/variant/utility_functions.hpp>
|
||||
|
||||
#include "python_stdlib.h"
|
||||
|
||||
@@ -63,6 +64,16 @@ String Metin2PythonHost::run_line(const String &source) {
|
||||
return String();
|
||||
}
|
||||
|
||||
// str(expression) from __main__, or "" when it raised (the error goes to the log like run_line's).
|
||||
String Metin2PythonHost::evaluate(const String &expression) {
|
||||
std::string result, error;
|
||||
if (!PythonBoot::Evaluate(expression.utf8().get_data(), &result, &error)) {
|
||||
UtilityFunctions::push_error(String::utf8(error.c_str()));
|
||||
return String();
|
||||
}
|
||||
return String::utf8(result.c_str());
|
||||
}
|
||||
|
||||
bool Metin2PythonHost::is_app_looping() { return PythonBoot::IsAppLooping(); }
|
||||
|
||||
void Metin2PythonHost::set_ui_size(int width, int height) { PythonBoot::SetUISize(width, height); }
|
||||
@@ -261,6 +272,8 @@ String Metin2PythonHost::run_line(const String &) {
|
||||
return String("this build has no embedded Python (-DMTGODOT_EMBED_PYTHON=ON)");
|
||||
}
|
||||
|
||||
String Metin2PythonHost::evaluate(const String &) { return String(); }
|
||||
|
||||
bool Metin2PythonHost::is_app_looping() { return false; }
|
||||
void Metin2PythonHost::set_ui_size(int, int) {}
|
||||
void Metin2PythonHost::ui_mouse_move(int, int) {}
|
||||
@@ -283,6 +296,7 @@ void Metin2PythonHost::_bind_methods() {
|
||||
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("start", "stdlib_path"),
|
||||
&Metin2PythonHost::start, DEFVAL(String()));
|
||||
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("is_running"), &Metin2PythonHost::is_running);
|
||||
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("evaluate", "expression"), &Metin2PythonHost::evaluate);
|
||||
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("run_main_script", "command_line"),
|
||||
&Metin2PythonHost::run_main_script, DEFVAL(String()));
|
||||
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("run_line", "source"), &Metin2PythonHost::run_line);
|
||||
|
||||
@@ -32,6 +32,7 @@ public:
|
||||
static godot::String run_main_script(const godot::String &command_line);
|
||||
// One line of Python in the same __main__ dictionary. "" when it ran.
|
||||
static godot::String run_line(const godot::String &source);
|
||||
static godot::String evaluate(const godot::String &expression);
|
||||
// True while system.py is inside app.Loop(); ui_update() then runs one CPythonApplication::Process().
|
||||
static bool is_app_looping();
|
||||
static void set_ui_size(int width, int height);
|
||||
|
||||
@@ -563,6 +563,24 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
|
||||
std::strncpy(ownership.owner, kCharacterName, sizeof(ownership.owner) - 1);
|
||||
if (!c.Send(ground) || !c.Send(ownership))
|
||||
return Fail(name + ": send ground item");
|
||||
// A monster in view: CHARACTER::EncodeInsertPacket sends ADDITIONAL_INFO only for PCs and NPCs, so a
|
||||
// CHAR_TYPE_MONSTER is ADD + UPDATE (the client names it from mob_proto).
|
||||
GCCharacterAdd mob = zeroed<GCCharacterAdd>();
|
||||
mob.header = HDR_GC_CHARACTER_ADD;
|
||||
mob.vid = kMobVID;
|
||||
mob.x = kMobX;
|
||||
mob.y = kMobY;
|
||||
mob.type = 0; // CHAR_TYPE_MONSTER
|
||||
mob.race = kMobRace;
|
||||
mob.moving_speed = 100;
|
||||
mob.attack_speed = 100;
|
||||
GCCharacterUpdate mob_update = zeroed<GCCharacterUpdate>();
|
||||
mob_update.header = HDR_GC_CHARACTER_UPDATE;
|
||||
mob_update.vid = kMobVID;
|
||||
mob_update.moving_speed = 100;
|
||||
mob_update.attack_speed = 100;
|
||||
if (!c.Send(mob) || !c.Send(mob_update))
|
||||
return Fail(name + ": send monster");
|
||||
Event(name + ":burst_sent");
|
||||
|
||||
// A ping after the burst proves that GamePhase consumed every packet above and can reply.
|
||||
@@ -575,6 +593,7 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
|
||||
// Then hold the connection until the test stops us, reading what the game phase sends the way
|
||||
// CInputProcessor::Process frames it (static size or TDynamicSizePacketHeader, then the sequence
|
||||
// byte). A CG_MOVE (input_main.cpp CInputMain::Move) becomes "<name>:move <func> <arg> <rot> <x> <y>".
|
||||
int mob_hp = kMobHits;
|
||||
for (;;)
|
||||
{
|
||||
std::uint8_t packet[1024] = {};
|
||||
@@ -671,6 +690,36 @@ bool FakeLoginServer::ServeGame(Connection& c, int index)
|
||||
if (!c.Send(chat) || !c.Send(line.c_str(), line.size() + 1))
|
||||
return Fail(name + ": send GC_CHAT");
|
||||
}
|
||||
// CInputMain::Target → CHARACTER::SetTarget → BroadcastTargetPacket: GC_TARGET with the monster's HP percent.
|
||||
if (header == HDR_CG_TARGET)
|
||||
{
|
||||
CGTarget target;
|
||||
std::memcpy(&target, packet, sizeof(target));
|
||||
Event(name + ":target " + std::to_string(target.vid));
|
||||
GCTarget out = {HDR_GC_TARGET, target.vid,
|
||||
static_cast<std::uint8_t>(target.vid == kMobVID ? mob_hp * 100 / kMobHits : 0)};
|
||||
if (!c.Send(out))
|
||||
return Fail(name + ": send GC_TARGET");
|
||||
}
|
||||
// CInputMain::Attack → battle_hit → CHARACTER::Damage: GC_DAMAGE_INFO to the attacker, the target HP
|
||||
// update, and at 0 HP CHARACTER::Dead's GC_DEAD.
|
||||
if (header == HDR_CG_ATTACK)
|
||||
{
|
||||
CGAttack attack;
|
||||
std::memcpy(&attack, packet, sizeof(attack));
|
||||
Event(name + ":attack " + std::to_string(attack.type) + " " + std::to_string(attack.victim_vid));
|
||||
if (attack.victim_vid == kMobVID && mob_hp > 0)
|
||||
{
|
||||
--mob_hp;
|
||||
GCDamageInfo damage = {HDR_GC_DAMAGE_INFO, kMobVID, 1 /* DAMAGE_NORMAL */, 100};
|
||||
GCTarget target = {HDR_GC_TARGET, kMobVID, static_cast<std::uint8_t>(mob_hp * 100 / kMobHits)};
|
||||
if (!c.Send(damage) || !c.Send(target))
|
||||
return Fail(name + ": send GC_DAMAGE_INFO");
|
||||
GCDead dead = {HDR_GC_DEAD, kMobVID};
|
||||
if (mob_hp == 0 && !c.Send(dead))
|
||||
return Fail(name + ": send GC_DEAD");
|
||||
}
|
||||
}
|
||||
if (header == HDR_CG_MOVE)
|
||||
{
|
||||
CGMove move;
|
||||
|
||||
@@ -26,6 +26,11 @@ public:
|
||||
static constexpr std::uint32_t kGroundItemVnum = 19;
|
||||
static constexpr std::int32_t kGroundItemX = kX + 150, kGroundItemY = kY;
|
||||
static constexpr std::uint32_t kExchangeItemVnum = 19, kExchangeGold = 1000;
|
||||
// A stray dog (npclist 101) for the combat step; three hits kill it. The spawn point lies in the village's
|
||||
// ATTRIBUTE_BANPK disc (CInstanceBase::IsInSafe blocks every hit there), so the dog stands 18 m north of it.
|
||||
static constexpr std::uint32_t kMobVID = 0x00023456, kMobRace = 101;
|
||||
static constexpr std::int32_t kMobX = kX, kMobY = kY - 1800;
|
||||
static constexpr int kMobHits = 3;
|
||||
|
||||
~FakeLoginServer();
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
#include "GameLib/FlyingData.h"
|
||||
#include "GameLib/FlyingObjectManager.h"
|
||||
#include "EffectLib/EffectManager.h"
|
||||
#include "EterLib/Camera.h"
|
||||
#include "GameLib/ItemManager.h"
|
||||
#include "UserInterface/PythonItem.h"
|
||||
#include "UserInterface/StdAfx.h"
|
||||
@@ -596,7 +597,7 @@ int main(int argc, char** argv)
|
||||
};
|
||||
std::printf("port_login_flow_test: %s\n", tail_info().c_str());
|
||||
CHECK(tail_info().find("ItemTail (Map 1,") != std::string::npos);
|
||||
CHECK(tail_info().find("ChrTail (Map 1,") != std::string::npos);
|
||||
CHECK(tail_info().find("ChrTail (Map 2,") != std::string::npos); // the actor and the stray dog
|
||||
// Holding ALT (game.GameWindow.ShowName) makes OnRender show every tail in range and project it after
|
||||
// app.RenderGame; the item tail keeps that screen rect, which is what textTail.Pick tests.
|
||||
PythonBoot::UIKey(DIK_LMENU, true);
|
||||
@@ -682,6 +683,57 @@ int main(int argc, char** argv)
|
||||
std::printf("port_login_flow_test: %s\n", e.c_str());
|
||||
}
|
||||
}
|
||||
// 9. (批次 4-b) Combat: a left click on the stray dog reaches CPythonPlayer::__OnClickActor, which targets it
|
||||
// (CG_TARGET → GC_TARGET, uitarget shows the HP board) and walks into range to attack. Each attack frame's
|
||||
// hit (CPythonPlayerEventHandler::OnHit) sends CG_ATTACK; the fake server answers GC_DAMAGE_INFO + GC_TARGET,
|
||||
// and GC_DEAD on the third hit.
|
||||
if (server)
|
||||
{
|
||||
CInstanceBase* mob = CPythonCharacterManager::Instance().GetInstancePtr(FakeLoginServer::kMobVID);
|
||||
CHECK(mob != nullptr);
|
||||
if (mob)
|
||||
{
|
||||
CHECK(!mob->IsDead());
|
||||
// Project the dog's body with RenderGame's perspective and the current camera (after the frame the
|
||||
// graphic state holds the UI's ortho projection).
|
||||
auto dog_on_screen = [&] {
|
||||
UI::CWindowManager& wm = UI::CWindowManager::Instance();
|
||||
D3DXMATRIX proj, identity;
|
||||
D3DXMatrixPerspectiveFovRH(&proj, D3DXToRadian(30.0f), wm.GetAspect(), 100.0f,
|
||||
CPythonBackground::Instance().GetFarClip());
|
||||
D3DXMatrixIdentity(&identity);
|
||||
D3DVIEWPORT8 viewport = {0, 0, (DWORD) wm.GetScreenWidth(), (DWORD) wm.GetScreenHeight(), 0.0f, 1.0f};
|
||||
const D3DXVECTOR3 body = mob->GetGraphicThingInstanceRef().GetPosition() + D3DXVECTOR3(0.0f, 0.0f, 40.0f);
|
||||
D3DXVECTOR3 screen;
|
||||
D3DXVec3Project(&screen, &body, &viewport, &proj,
|
||||
&CCameraManager::Instance().GetCurrentCamera()->GetViewMatrix(), &identity);
|
||||
return std::make_pair((int) screen.x, (int) screen.y);
|
||||
};
|
||||
auto [sx, sy] = dog_on_screen();
|
||||
std::printf("port_login_flow_test: stray dog on screen at %d %d\n", sx, sy);
|
||||
PythonBoot::UIMouseMove(sx, sy);
|
||||
pump_until(0.1, [] { return false; });
|
||||
// game.GameWindow.OnMouseLeftButtonDown → player.SetMouseState(MBT_LEFT, MBS_PRESS): MBF_SMART on an
|
||||
// actor is __OnPressActor, which walks into range (__ReserveClickActor) and attacks while the button is
|
||||
// held; the cursor follows the dog as the camera follows the actor, as a player's hand would.
|
||||
PythonBoot::UIMouseButton(1, true, sx, sy);
|
||||
const std::string mob_vid = std::to_string(FakeLoginServer::kMobVID);
|
||||
CHECK(pump_until(2, [&] { return server->Has("game2:target " + mob_vid); }));
|
||||
CHECK(pump_until(2, [] { return py_true("_stream.curPhaseWindow.targetBoard.IsShow()"); }));
|
||||
CHECK(pump_until(10, [&] {
|
||||
std::tie(sx, sy) = dog_on_screen();
|
||||
PythonBoot::UIMouseMove(sx, sy);
|
||||
return mob->IsDead();
|
||||
}));
|
||||
PythonBoot::UIMouseButton(1, false, sx, sy);
|
||||
size_t attacks = 0;
|
||||
for (const auto& e : server->Events())
|
||||
if (e == "game2:attack 0 " + mob_vid)
|
||||
++attacks;
|
||||
std::printf("port_login_flow_test: %zu attack(s) until the stray dog died\n", attacks);
|
||||
CHECK(attacks >= (size_t) FakeLoginServer::kMobHits);
|
||||
}
|
||||
}
|
||||
if (server)
|
||||
{
|
||||
const std::vector<std::string> expected = {
|
||||
@@ -700,7 +752,8 @@ int main(int argc, char** argv)
|
||||
for (const auto& e : events)
|
||||
if (e.rfind(prefix, 0) == 0 && e.find(":move ") == std::string::npos &&
|
||||
e.find(":pickup ") == std::string::npos &&
|
||||
e.find(":exchange ") == std::string::npos && e.find(":chat ") == std::string::npos)
|
||||
e.find(":exchange ") == std::string::npos && e.find(":chat ") == std::string::npos &&
|
||||
e.find(":target ") == std::string::npos && e.find(":attack ") == std::string::npos)
|
||||
got.push_back(e);
|
||||
CHECK(want == got);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user