2V3-b: click on the ground to walk; read the terrain attribute map
CScreen::SetCursorPosition and CPythonGraphic::SetCursorPosition are the 40250 bodies, so RenderGame builds the pick ray under the cursor each frame. CPythonBackground::GetPickingPoint* march that ray as CMapOutdoor::__PickTerrainHeight does, against the native CMapManager height (PR_FLOAT_TO_INT's x87 rounding becomes floor); object picking waits for CMapOutdoor. CMapManager::isAttrOn reads the packed <map>/<xxxyyy>/attr.atr as CMapOutdoor::isAttrOn + CTerrain::isAttrOn do, and CheckAdvancing with its collision checkers is verbatim (no map objects are registered yet). All of this stays in the pending/platform layer and is marked ADAPTED. port.login_flow clicks the ground below the actor after the key walk. The actor walks to the picked point (334 cm offline), and the fake server sees FUNC_MOVE, then FUNC_WAIT. It also checks that the actor's cell is free and that its terrain block has blocked cells. port.login_live against the real server passed (key 133 cm, click 296 cm), and python_game_render_test.sh passes offline. MT_TEST_MODE=python_ui now adds the 3D surface, so the port route can be played by hand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ff0983d3c8
commit
8b4700f008
@@ -244,9 +244,35 @@ auto CScreen::SetClearStencil(DWORD) -> void
|
||||
MT_PLATFORM_STUB();
|
||||
}
|
||||
|
||||
auto CScreen::SetCursorPosition(int, int, int, int) -> void
|
||||
// 40250 GrpScreen.cpp:487, verbatim: the pick ray through the cursor.
|
||||
void CScreen::SetCursorPosition(int x, int y, int hres, int vres)
|
||||
{
|
||||
MT_PLATFORM_STUB();
|
||||
D3DXVECTOR3 v;
|
||||
v.x = -(((2.0f * x) / hres) - 1) / ms_matProj._11;
|
||||
v.y = (((2.0f * y) / vres) - 1) / ms_matProj._22;
|
||||
v.z = 1.0f;
|
||||
|
||||
D3DXMATRIX matViewInverse=ms_matInverseView;
|
||||
//D3DXMatrixInverse(&matViewInverse, NULL, &ms_matView);
|
||||
|
||||
ms_vtPickRayDir.x = v.x * matViewInverse._11 +
|
||||
v.y * matViewInverse._21 +
|
||||
v.z * matViewInverse._31;
|
||||
|
||||
ms_vtPickRayDir.y = v.x * matViewInverse._12 +
|
||||
v.y * matViewInverse._22 +
|
||||
v.z * matViewInverse._32;
|
||||
|
||||
ms_vtPickRayDir.z = v.x * matViewInverse._13 +
|
||||
v.y * matViewInverse._23 +
|
||||
v.z * matViewInverse._33;
|
||||
|
||||
ms_vtPickRayOrig.x = matViewInverse._41;
|
||||
ms_vtPickRayOrig.y = matViewInverse._42;
|
||||
ms_vtPickRayOrig.z = matViewInverse._43;
|
||||
|
||||
ms_Ray.SetStartPoint(ms_vtPickRayOrig);
|
||||
ms_Ray.SetDirection(-ms_vtPickRayDir, 51200.0f);
|
||||
}
|
||||
|
||||
auto CScreen::GetCursorPosition(float *, float *, float *) -> bool
|
||||
|
||||
@@ -36,7 +36,11 @@ long CPythonGraphic::GenerateColor(float r, float g, float b, float a) { return
|
||||
void CPythonGraphic::RenderUpButton(float, float, float, float) { MT_PLATFORM_STUB(); }
|
||||
void CPythonGraphic::RenderDownButton(float, float, float, float) { MT_PLATFORM_STUB(); }
|
||||
bool CPythonGraphic::SaveScreenShot(const char*) { MT_PLATFORM_STUB(); return false; }
|
||||
void CPythonGraphic::SetCursorPosition(int, int) { MT_PLATFORM_STUB(); }
|
||||
// 40250 PythonGraphic.cpp:52.
|
||||
void CPythonGraphic::SetCursorPosition(int x, int y)
|
||||
{
|
||||
CScreen::SetCursorPosition(x, y, ms_iWidth, ms_iHeight);
|
||||
}
|
||||
DWORD CPythonGraphic::GetAvailableMemory() { MT_PLATFORM_STUB(); return 0; }
|
||||
void CPythonGraphic::SetGamma(float) { MT_PLATFORM_STUB(); }
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ struct NativeMapState {
|
||||
fmt::MapSetting setting;
|
||||
std::string name;
|
||||
std::map<std::pair<int, int>, fmt::HeightMap> heights;
|
||||
std::map<std::pair<int, int>, std::vector<BYTE>> attrs; // attr.atr payload, empty when unreadable
|
||||
bool ready = false;
|
||||
};
|
||||
std::map<const CMapManager *, NativeMapState> s_native_maps;
|
||||
@@ -61,6 +62,28 @@ bool load_height_tile(NativeMapState &state, int tx, int ty)
|
||||
state.heights.emplace(key, std::move(map));
|
||||
return true;
|
||||
}
|
||||
|
||||
// attr.atr: {u16 magic=2634, u16 w=256, u16 h=256} + 256*256 bytes (40250 CTerrainImpl::LoadAttrMap).
|
||||
const std::vector<BYTE> *attr_tile(NativeMapState &state, int tx, int ty)
|
||||
{
|
||||
if (tx < 0 || ty < 0 || tx >= state.setting.map_size_x || ty >= state.setting.map_size_y)
|
||||
return nullptr;
|
||||
const auto key = std::make_pair(tx, ty);
|
||||
auto found = state.attrs.find(key);
|
||||
if (found == state.attrs.end())
|
||||
{
|
||||
char tile[16];
|
||||
snprintf(tile, sizeof(tile), "%03d%03d", tx, ty);
|
||||
std::string bytes;
|
||||
std::vector<BYTE> attr;
|
||||
const size_t payload = size_t(CTerrainImpl::ATTRMAP_XSIZE) * CTerrainImpl::ATTRMAP_YSIZE;
|
||||
if (pack_bytes(state.name + "/" + tile + "/attr.atr", bytes) && bytes.size() == 6 + payload &&
|
||||
(static_cast<unsigned char>(bytes[0]) | (static_cast<unsigned char>(bytes[1]) << 8)) == 2634)
|
||||
attr.assign(bytes.begin() + 6, bytes.end());
|
||||
found = state.attrs.emplace(key, std::move(attr)).first;
|
||||
}
|
||||
return found->second.empty() ? nullptr : &found->second;
|
||||
}
|
||||
}
|
||||
|
||||
auto CMapManager::GetHeight(float fx, float fy) -> float
|
||||
@@ -194,16 +217,56 @@ auto CMapManager::GetShadowMapColor(float, float) -> DWORD
|
||||
return mt_platform_stub_return<DWORD>();
|
||||
}
|
||||
|
||||
auto CMapManager::isAttrOn(float, float, BYTE) -> bool
|
||||
// 40250 MapManager.cpp:506 → CMapOutdoor::isAttrOn (MapOutdoor.cpp:1202).
|
||||
bool CMapManager::isAttrOn(float fX, float fY, BYTE byAttr)
|
||||
{
|
||||
MT_PLATFORM_STUB();
|
||||
return mt_platform_stub_return<bool>();
|
||||
if (!IsMapReady())
|
||||
return false;
|
||||
|
||||
// PORT: PR_FLOAT_TO_INT is x87 fistp (round to nearest) corrected down, i.e. floor.
|
||||
return isAttrOn(int(std::floor(fX)), int(std::floor(fY)), byAttr);
|
||||
}
|
||||
|
||||
auto CMapManager::isAttrOn(int, int, BYTE) -> bool
|
||||
// 40250 MapManager.cpp:524 → CMapOutdoor::isAttrOn (MapOutdoor.cpp:1220) → CTerrain::isAttrOn (AreaTerrain.cpp:487).
|
||||
// PORT: CMapOutdoor/CTerrain are not ported; the terrain count and the attribute map come from the native map
|
||||
// state (Setting.txt MapSize, <map>/<xxxyyy>/attr.atr) instead of the loaded CTerrain list.
|
||||
bool CMapManager::isAttrOn(int iX, int iY, BYTE byAttr)
|
||||
{
|
||||
MT_PLATFORM_STUB();
|
||||
return mt_platform_stub_return<bool>();
|
||||
if (!IsMapReady())
|
||||
return false;
|
||||
auto state = s_native_maps.find(this);
|
||||
if (state == s_native_maps.end() || !state->second.ready)
|
||||
return false;
|
||||
const int countX = state->second.setting.map_size_x, countY = state->second.setting.map_size_y;
|
||||
if (iX < 0 || iY < 0 || iX > countX * CTerrainImpl::TERRAIN_XSIZE || iY > countY * CTerrainImpl::TERRAIN_YSIZE)
|
||||
return false;
|
||||
|
||||
WORD wTerrainCoordX, wTerrainCoordY;
|
||||
wTerrainCoordX = iX / CTerrainImpl::TERRAIN_XSIZE;
|
||||
wTerrainCoordY = iY / CTerrainImpl::TERRAIN_YSIZE;
|
||||
|
||||
const std::vector<BYTE> *attr = attr_tile(state->second, wTerrainCoordX, wTerrainCoordY);
|
||||
if (!attr)
|
||||
return false;
|
||||
|
||||
WORD wLocalX, wLocalY;
|
||||
wLocalX = (iX - wTerrainCoordX * CTerrainImpl::TERRAIN_XSIZE) / (CTerrainImpl::HALF_CELLSCALE);
|
||||
wLocalY = (iY - wTerrainCoordY * CTerrainImpl::TERRAIN_YSIZE) / (CTerrainImpl::HALF_CELLSCALE);
|
||||
|
||||
if (wLocalX >= CTerrainImpl::ATTRMAP_XSIZE || wLocalY >= CTerrainImpl::ATTRMAP_YSIZE)
|
||||
return false;
|
||||
|
||||
BYTE byMapAttr = (*attr)[wLocalY * CTerrainImpl::ATTRMAP_XSIZE + wLocalX];
|
||||
|
||||
if ( byAttr < 16 )
|
||||
return (byMapAttr & byAttr) ? true : false;
|
||||
else
|
||||
{
|
||||
if ( byAttr/16 == byMapAttr/16)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto CMapManager::BeginEnvironment() -> void
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// (docs/PORT-PLAN.md).
|
||||
#include "UserInterface/StdAfx.h"
|
||||
#include "UserInterface/PythonBackground.h"
|
||||
#include "UserInterface/InstanceBase.h"
|
||||
#include "EterLib/StateManager.h"
|
||||
|
||||
#include "../../PlatformStub.h"
|
||||
@@ -107,16 +108,143 @@ CPythonBackground::~CPythonBackground()
|
||||
MT_PLATFORM_STUB();
|
||||
}
|
||||
|
||||
auto CPythonBackground::CheckAdvancing(CInstanceBase *) -> bool
|
||||
// 40250 PythonBackground.cpp:547-608, verbatim. No map objects are registered with CCullingManager until
|
||||
// CMapOutdoor is ported, so the checkers see no opponent and advancing is never blocked.
|
||||
struct CollisionChecker
|
||||
{
|
||||
MT_PLATFORM_STUB();
|
||||
return mt_platform_stub_return<bool>();
|
||||
bool isBlocked;
|
||||
CInstanceBase* pInstance;
|
||||
CollisionChecker(CInstanceBase* pInstance) : pInstance(pInstance), isBlocked(false) {}
|
||||
void operator () (CGraphicObjectInstance* pOpponent)
|
||||
{
|
||||
if (isBlocked)
|
||||
return;
|
||||
|
||||
if (!pOpponent)
|
||||
return;
|
||||
|
||||
if (pInstance->IsBlockObject(*pOpponent))
|
||||
isBlocked=true;
|
||||
}
|
||||
};
|
||||
|
||||
struct CollisionAdjustChecker
|
||||
{
|
||||
bool isBlocked;
|
||||
CInstanceBase* pInstance;
|
||||
CollisionAdjustChecker(CInstanceBase* pInstance) : pInstance(pInstance), isBlocked(false) {}
|
||||
void operator () (CGraphicObjectInstance* pOpponent)
|
||||
{
|
||||
if (!pOpponent)
|
||||
return;
|
||||
|
||||
if (pInstance->AvoidObject(*pOpponent))
|
||||
isBlocked=true;
|
||||
}
|
||||
};
|
||||
|
||||
bool CPythonBackground::CheckAdvancing(CInstanceBase * pInstance)
|
||||
{
|
||||
if (!IsMapReady())
|
||||
return true;
|
||||
|
||||
Vector3d center;
|
||||
float radius;
|
||||
pInstance->GetGraphicThingInstanceRef().GetBoundingSphere(center,radius);
|
||||
|
||||
CCullingManager & rkCullingMgr = CCullingManager::Instance();
|
||||
|
||||
CollisionAdjustChecker kCollisionAdjustChecker(pInstance);
|
||||
rkCullingMgr.ForInRange(center, radius, &kCollisionAdjustChecker);
|
||||
if (kCollisionAdjustChecker.isBlocked)
|
||||
{
|
||||
CollisionChecker kCollisionChecker(pInstance);
|
||||
rkCullingMgr.ForInRange(center, radius, &kCollisionChecker);
|
||||
if (kCollisionChecker.isBlocked)
|
||||
{
|
||||
pInstance->BlockMovement();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
pInstance->NEW_MoveToDestPixelPositionDirection(pInstance->NEW_GetDstPixelPositionRef());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto CPythonBackground::GetPickingPointWithRayOnlyTerrain(const CRay &, D3DXVECTOR3 *) -> bool
|
||||
namespace
|
||||
{
|
||||
MT_PLATFORM_STUB();
|
||||
return mt_platform_stub_return<bool>();
|
||||
// 40250 CMapOutdoor::__PickTerrainHeight (MapOutdoor.cpp:448), marching the ray against the terrain height.
|
||||
// PORT: CMapOutdoor is not ported; the terrain is the native CMapManager height map, and GetTerrainNum/
|
||||
// GetTerrainPointer become "the map is ready" (GetHeight reads the height tile under the point).
|
||||
bool PickTerrainHeight(CMapManager& rkMap, float& fPos, const D3DXVECTOR3& v3Start, const D3DXVECTOR3& v3End, float fStep,
|
||||
float fRayRange, float fLimitRange, D3DXVECTOR3* pv3Pick)
|
||||
{
|
||||
D3DXVECTOR3 v3CurPos;
|
||||
|
||||
float fRayRangeInv=1.0f/fRayRange;
|
||||
while (fPos < fRayRange && fPos<fLimitRange)
|
||||
{
|
||||
D3DXVec3Lerp(&v3CurPos, &v3Start, &v3End, fPos*fRayRangeInv);
|
||||
float fMultiplier = 1.0f;
|
||||
if (rkMap.IsMapReady())
|
||||
{
|
||||
// PORT: PR_FLOAT_TO_INT is x87 fistp (round to nearest) corrected down, i.e. floor.
|
||||
int ix = int(std::floor(v3CurPos.x)), iy = int(std::floor(fabs(v3CurPos.y)));
|
||||
float fMapHeight = rkMap.GetHeight(float(ix), float(iy));
|
||||
if ( fMapHeight >= v3CurPos.z)
|
||||
{
|
||||
*pv3Pick = v3CurPos;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
fMultiplier = fMAX(1.0f, 0.01f * ( v3CurPos.z - fMapHeight ) );
|
||||
}
|
||||
}
|
||||
fPos += fStep * fMultiplier;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 40250 CMapOutdoor::GetPickingPointWithRayOnlyTerrain (MapOutdoor.cpp:549).
|
||||
bool CPythonBackground::GetPickingPointWithRayOnlyTerrain(const CRay & rRay, D3DXVECTOR3 * v3IntersectPt)
|
||||
{
|
||||
bool bTerrainPick = false;
|
||||
D3DXVECTOR3 v3TerrainPick;
|
||||
|
||||
D3DXVECTOR3 v3Start, v3End, v3Dir;
|
||||
float fRayRange;
|
||||
rRay.GetStartPoint(&v3Start);
|
||||
rRay.GetDirection(&v3Dir, &fRayRange);
|
||||
rRay.GetEndPoint(&v3End);
|
||||
|
||||
float fPos = 0.0f;
|
||||
bTerrainPick=true;
|
||||
if (!PickTerrainHeight(*this, fPos, v3Start, v3End, 5.0f, fRayRange, 5000.0f, &v3TerrainPick))
|
||||
if (!PickTerrainHeight(*this, fPos, v3Start, v3End, 10.0f, fRayRange, 10000.0f, &v3TerrainPick))
|
||||
if (!PickTerrainHeight(*this, fPos, v3Start, v3End, 100.0f, fRayRange, 100000.0f, &v3TerrainPick))
|
||||
bTerrainPick=false;
|
||||
|
||||
if (bTerrainPick)
|
||||
{
|
||||
*v3IntersectPt = v3TerrainPick;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 40250 CMapOutdoor::GetPickingPointWithRay (MapOutdoor.cpp:484).
|
||||
// PORT: the object pick (FGetPickingPoint over CCullingManager) needs CMapOutdoor's area objects; only the
|
||||
// terrain half runs until the map is ported.
|
||||
bool CPythonBackground::GetPickingPointWithRay(const CRay & rRay, D3DXVECTOR3 * v3IntersectPt)
|
||||
{
|
||||
return GetPickingPointWithRayOnlyTerrain(rRay, v3IntersectPt);
|
||||
}
|
||||
|
||||
// 40250 PythonBackground.cpp:173 (CPythonApplication::RenderGame's SetPerspective); m_pkMap stays null
|
||||
@@ -207,8 +335,8 @@ void CPythonBackground::Initialize()
|
||||
CMapManager::Initialize();
|
||||
}
|
||||
|
||||
auto CPythonBackground::GetPickingPoint(D3DXVECTOR3 *) -> bool
|
||||
// 40250 PythonBackground.cpp:263 → CMapOutdoor::GetPickingPoint (MapOutdoor.cpp:443): the cursor ray.
|
||||
bool CPythonBackground::GetPickingPoint(D3DXVECTOR3 * v3IntersectPt)
|
||||
{
|
||||
MT_PLATFORM_STUB();
|
||||
return mt_platform_stub_return<bool>();
|
||||
return GetPickingPointWithRay(ms_Ray, v3IntersectPt);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
#include "UserInterface/StdAfx.h"
|
||||
#include "UserInterface/PythonCharacterManager.h"
|
||||
#include "UserInterface/InstanceBase.h"
|
||||
#include "UserInterface/PythonBackground.h"
|
||||
#include "EterGrnLib/StdAfx.h"
|
||||
#include "EterGrnLib/LODController.h"
|
||||
#include "EterGrnLib/ModelInstance.h"
|
||||
@@ -413,6 +414,54 @@ int main(int argc, char** argv)
|
||||
for (const auto& e : m)
|
||||
std::printf("port_login_flow_test: %s\n", e.c_str());
|
||||
}
|
||||
|
||||
// The terrain attribute map (attr.atr) under the actor: CInstanceBase's block/water/ban-PK checks read it
|
||||
// through CPythonBackground::isAttrOn. The actor stands on a free cell; its 256x256 tile has blocked cells.
|
||||
if (main_instance)
|
||||
{
|
||||
TPixelPosition here = {};
|
||||
main_instance->NEW_GetPixelPosition(&here);
|
||||
CPythonBackground& bg = CPythonBackground::Instance();
|
||||
CHECK(!bg.isAttrOn(here.x, here.y, CTerrainImpl::ATTRIBUTE_BLOCK));
|
||||
const int tx = int(here.x) / CTerrainImpl::TERRAIN_XSIZE * CTerrainImpl::TERRAIN_XSIZE;
|
||||
const int ty = int(here.y) / CTerrainImpl::TERRAIN_YSIZE * CTerrainImpl::TERRAIN_YSIZE;
|
||||
int blocked = 0;
|
||||
for (int y = 0; y < CTerrainImpl::TERRAIN_YSIZE; y += CTerrainImpl::HALF_CELLSCALE)
|
||||
for (int x = 0; x < CTerrainImpl::TERRAIN_XSIZE; x += CTerrainImpl::HALF_CELLSCALE)
|
||||
blocked += bg.isAttrOn(tx + x, ty + y, CTerrainImpl::ATTRIBUTE_BLOCK) ? 1 : 0;
|
||||
std::printf("port_login_flow_test: %d blocked attribute cells in the actor's terrain\n", blocked);
|
||||
CHECK(blocked > 0);
|
||||
}
|
||||
|
||||
// 8. (批次 2V3) Click-to-move: a left click on the ground below the actor reaches game.GameWindow's
|
||||
// OnMouseLeftButtonDown → player.SetMouseState(MBT_LEFT, MBS_PRESS); CPythonPlayer picks the ground
|
||||
// through the cursor ray (CScreen::SetCursorPosition → CPythonBackground::GetPickingPoint) and the
|
||||
// actor walks to it, sending CG_MOVE FUNC_MOVE, then FUNC_WAIT on arrival.
|
||||
const size_t moves_before = server ? server->Events().size() : 0;
|
||||
if (main_instance)
|
||||
main_instance->NEW_GetPixelPosition(&start);
|
||||
PythonBoot::UIMouseButton(1, true, 400, 520);
|
||||
pump_until(0.1, [] { return false; });
|
||||
PythonBoot::UIMouseButton(1, false, 400, 520);
|
||||
CHECK(main_instance && pump_until(3, [&] { return walked() > 100.0f; }));
|
||||
CHECK(main_instance && pump_until(5, [&] { return !main_instance->IsWalking(); }));
|
||||
std::printf("port_login_flow_test: click walked %.0f cm\n", main_instance ? walked() : 0.0f);
|
||||
if (server)
|
||||
{
|
||||
auto click_moves = [&] {
|
||||
std::vector<std::string> out;
|
||||
const auto events = server->Events();
|
||||
for (size_t i = moves_before; i < events.size(); ++i)
|
||||
if (events[i].rfind("game2:move ", 0) == 0)
|
||||
out.push_back(events[i]);
|
||||
return out;
|
||||
};
|
||||
CHECK(pump_until(2, [&] { auto m = click_moves(); return m.size() >= 2 && m.back().rfind("game2:move 0 ", 0) == 0; }));
|
||||
const auto m = click_moves();
|
||||
CHECK(!m.empty() && m.front().rfind("game2:move 1 0 ", 0) == 0);
|
||||
for (const auto& e : m)
|
||||
std::printf("port_login_flow_test: %s\n", e.c_str());
|
||||
}
|
||||
}
|
||||
if (server)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user