render and server locale parity fixes
This commit is contained in:
@@ -6,6 +6,28 @@
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
struct Point { float x, y; };
|
||||
std::vector<Point> clip_polygon(std::vector<Point> polygon, int edge, float bound) {
|
||||
std::vector<Point> result;
|
||||
if (polygon.empty()) return result;
|
||||
auto inside = [edge, bound](Point p) {
|
||||
return edge == 0 ? p.x >= bound : edge == 1 ? p.x <= bound
|
||||
: edge == 2 ? p.y >= bound : p.y <= bound;
|
||||
};
|
||||
auto intersect = [edge, bound](Point a, Point b) {
|
||||
const float t = edge < 2 ? (bound - a.x) / (b.x - a.x)
|
||||
: (bound - a.y) / (b.y - a.y);
|
||||
return Point{a.x + t * (b.x - a.x), a.y + t * (b.y - a.y)};
|
||||
};
|
||||
Point previous = polygon.back();
|
||||
for (Point current : polygon) {
|
||||
const bool was_inside = inside(previous), is_inside = inside(current);
|
||||
if (was_inside != is_inside) result.push_back(intersect(previous, current));
|
||||
if (is_inside) result.push_back(current);
|
||||
previous = current;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
std::vector<UIRenderCommand> commands;
|
||||
std::uint64_t frame_id = 0;
|
||||
int canvas_width = 0;
|
||||
@@ -31,6 +53,32 @@ void UIRenderAdd(UIRenderCommand command) {
|
||||
command.clip_x1 = clip_x1; command.clip_y1 = clip_y1;
|
||||
command.clip_x2 = clip_x2; command.clip_y2 = clip_y2;
|
||||
command.behind_3d = Render3DDraws().empty();
|
||||
if (command.kind == UIRenderCommand::Bar && command.quad) {
|
||||
// Cooldown fans are triangles. Clip their geometry before either renderer consumes it.
|
||||
std::vector<Point> polygon{{command.qx[0], command.qy[0]},
|
||||
{command.qx[1], command.qy[1]},
|
||||
{command.qx[2], command.qy[2]}};
|
||||
const float bounds[4] = {clip_x1, clip_x2, clip_y1, clip_y2};
|
||||
for (int edge = 0; edge < 4; ++edge)
|
||||
polygon = clip_polygon(std::move(polygon), edge, bounds[edge]);
|
||||
for (std::size_t i = 1; i + 1 < polygon.size(); ++i) {
|
||||
UIRenderCommand piece = command;
|
||||
const Point tri[3] = {polygon[0], polygon[i], polygon[i + 1]};
|
||||
const float area = (tri[1].x - tri[0].x) * (tri[2].y - tri[0].y) -
|
||||
(tri[1].y - tri[0].y) * (tri[2].x - tri[0].x);
|
||||
if (area == 0.0f) continue;
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
piece.qx[k] = tri[k < 3 ? k : 2].x;
|
||||
piece.qy[k] = tri[k < 3 ? k : 2].y;
|
||||
}
|
||||
piece.x1 = std::min({tri[0].x, tri[1].x, tri[2].x});
|
||||
piece.y1 = std::min({tri[0].y, tri[1].y, tri[2].y});
|
||||
piece.x2 = std::max({tri[0].x, tri[1].x, tri[2].x});
|
||||
piece.y2 = std::max({tri[0].y, tri[1].y, tri[2].y});
|
||||
commands.push_back(std::move(piece));
|
||||
}
|
||||
return;
|
||||
}
|
||||
commands.push_back(command);
|
||||
}
|
||||
const std::vector<UIRenderCommand>& UIRenderCommands() { return commands; }
|
||||
|
||||
@@ -38,6 +38,9 @@ void CMapOutdoor::RenderWater()
|
||||
// PORT: see the file comment.
|
||||
if (!IsNativeTerrainRenderEnabled())
|
||||
return;
|
||||
CGraphicTexture* pWaterTexture = m_WaterInstances[((ELTimer_GetMSec() / 70) % 30)].GetTexturePointer();
|
||||
if (!pWaterTexture)
|
||||
return;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// RenderState
|
||||
@@ -49,7 +52,7 @@ void CMapOutdoor::RenderWater()
|
||||
STATEMANAGER.SaveRenderState(D3DRS_DIFFUSEMATERIALSOURCE, D3DMCS_COLOR1);
|
||||
STATEMANAGER.SaveRenderState(D3DRS_COLORVERTEX, TRUE);
|
||||
|
||||
STATEMANAGER.SetTexture(0, m_WaterInstances[((ELTimer_GetMSec() / 70) % 30)].GetTexturePointer()->GetD3DTexture());
|
||||
STATEMANAGER.SetTexture(0, pWaterTexture->GetD3DTexture());
|
||||
|
||||
D3DXMatrixScaling(&matTexTransformWater, m_fWaterTexCoordBase, -m_fWaterTexCoordBase, 0.0f);
|
||||
D3DXMatrixMultiply(&matTexTransformWater, &m_matViewInverse, &matTexTransformWater);
|
||||
|
||||
@@ -88,6 +88,8 @@ void UIIMEKeyDown(int vkey);
|
||||
// While IsAppLooping(), UIUpdate is AppFrame() and UIRender keeps the commands that Process() drew.
|
||||
void UIUpdate();
|
||||
void UIRender();
|
||||
// Completed native frame duration, including Vulkan rendering and presentation, for auto sight range.
|
||||
void SetNativeRenderFrameTime(float milliseconds);
|
||||
bool IsSoftwareCursorVisible();
|
||||
|
||||
// 40250 Main() calls Clear() and lets the launcher leave scope.
|
||||
|
||||
@@ -588,6 +588,11 @@ bool CSpeedTreeRT::LoadTree(const unsigned char * pBlock, unsigned int nNumBytes
|
||||
return true;
|
||||
}
|
||||
|
||||
void CSpeedTreeRT::SetSourceName(const char * pFilename)
|
||||
{
|
||||
m_pImpl->pTree->strName = pFilename ? pFilename : "";
|
||||
}
|
||||
|
||||
bool CSpeedTreeRT::Compute(const float * /*pTransform*/, unsigned int nSeed, bool /*bCompositeStrips*/)
|
||||
{
|
||||
STreeData & tree = *m_pImpl->pTree;
|
||||
|
||||
@@ -72,6 +72,8 @@ float CPythonApplication::GetGlobalElapsedTime() { return CTimer::Instance().Get
|
||||
static bool s_isFrameSkipDisable = false;
|
||||
// m_iForceSightRange (the 40250 constructor sets -1: automatic sight range).
|
||||
static int s_iForceSightRange = -1;
|
||||
static float s_nativeRenderFrameTime = 0.0f;
|
||||
void PythonBoot::SetNativeRenderFrameTime(float milliseconds) { s_nativeRenderFrameTime = milliseconds; }
|
||||
void CPythonApplication::SetFrameSkip(bool isEnable) { s_isFrameSkipDisable = !isEnable; }
|
||||
// PORT: 40250 creates the Win32 window, the D3D8 device, cursors, sound, keyboard, the net device,
|
||||
// fonts and the IME here. The Godot window and its canvas already exist when the host starts the
|
||||
@@ -350,7 +352,11 @@ bool CPythonApplication::Process()
|
||||
static DWORD s_dwRenderRangeTime = 0;
|
||||
static DWORD s_dwRenderRangeFrame = 0;
|
||||
|
||||
m_dwCurRenderTime = dwRenderEndTime - dwRenderStartTime;
|
||||
// The native host completes Vulkan work after this command-recording pass.
|
||||
// Use its last completed frame, including presentation, for automatic sight range.
|
||||
m_dwCurRenderTime = s_nativeRenderFrameTime > 0.0f
|
||||
? static_cast<DWORD>(s_nativeRenderFrameTime + 0.5f)
|
||||
: dwRenderEndTime - dwRenderStartTime;
|
||||
s_dwRenderRangeTime += m_dwCurRenderTime;
|
||||
++s_dwRenderRangeFrame;
|
||||
|
||||
|
||||
@@ -222,6 +222,9 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
|
||||
add_test(NAME port.login_flow
|
||||
COMMAND $<TARGET_FILE:port_login_flow_test> ${MT_40250_CLIENT} ${MT_PYTHON_STDLIB_ZIP})
|
||||
set_tests_properties(port.login_flow PROPERTIES SKIP_RETURN_CODE 77)
|
||||
# Offline locale data builder: retains 40250 proto layout and gameplay fields.
|
||||
add_executable(locale_proto_patch ${CMAKE_CURRENT_SOURCE_DIR}/../../tests/locale_proto_patch.cpp)
|
||||
target_link_libraries(locale_proto_patch PRIVATE port_platform)
|
||||
# 2V2-e: the same fake server as a process, for the Godot-driven render run
|
||||
# (script/python_game_render_test.sh).
|
||||
add_executable(port_fake_login_server ${CMAKE_CURRENT_SOURCE_DIR}/../../tests/port_fake_login_server_main.cpp
|
||||
|
||||
@@ -371,6 +371,8 @@ bool CSpeedTreeWrapper::LoadTree(const char * pszSptFile, const BYTE * c_pbBlock
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// PORT: the in-memory SpeedTree replacement needs the pack path for species geometry and UVs.
|
||||
m_pSpeedTree->SetSourceName(pszSptFile);
|
||||
|
||||
// override the lighting method stored in the spt file
|
||||
#ifdef WRAPPER_USE_DYNAMIC_LIGHTING
|
||||
|
||||
@@ -297,6 +297,8 @@ static void operator delete[](void* pRawMemory);
|
||||
|
||||
bool LoadTree(const char* pFilename);
|
||||
bool LoadTree(const unsigned char* pBlock, unsigned int nNumBytes);
|
||||
// PORT: supply the pack path when LoadTree receives only a memory block.
|
||||
void SetSourceName(const char* pFilename);
|
||||
unsigned char* SaveTree(unsigned int& nNumBytes, bool bSaveLeaves = false) const;
|
||||
|
||||
void GetTreeSize(float& fSize, float& fVariance) const;
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
// Offline CP936 name overlay for the 40250 item/mob proto format.
|
||||
// Runtime loading remains the original 40250 path and binary format.
|
||||
#include "GameLib/StdAfx.h"
|
||||
#include "GameLib/ItemData.h"
|
||||
#include "UserInterface/StdAfx.h"
|
||||
#include "UserInterface/PythonNonPlayer.h"
|
||||
#include "EterBase/lzo.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
using Item = CItemData::TItemTable;
|
||||
using Mob = CPythonNonPlayer::TMobTable;
|
||||
|
||||
constexpr std::array<DWORD, 4> item_key{{173217, 72619434, 408587239, 27973291}};
|
||||
constexpr std::array<DWORD, 4> mob_key{{4813894, 18955, 552631, 6822045}};
|
||||
|
||||
std::vector<BYTE> read_file(const char* path) {
|
||||
std::ifstream stream(path, std::ios::binary);
|
||||
if (!stream) throw std::runtime_error(std::string("cannot read ") + path);
|
||||
return {std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>()};
|
||||
}
|
||||
|
||||
void write_file(const char* path, const std::vector<BYTE>& bytes) {
|
||||
std::ofstream stream(path, std::ios::binary | std::ios::trunc);
|
||||
if (!stream || !stream.write(reinterpret_cast<const char*>(bytes.data()), bytes.size()))
|
||||
throw std::runtime_error(std::string("cannot write ") + path);
|
||||
}
|
||||
|
||||
DWORD word(const std::vector<BYTE>& bytes, std::size_t offset) {
|
||||
if (offset + 4 > bytes.size()) throw std::runtime_error("truncated proto header");
|
||||
DWORD value;
|
||||
std::memcpy(&value, bytes.data() + offset, 4);
|
||||
return value;
|
||||
}
|
||||
|
||||
void put_word(std::vector<BYTE>& bytes, std::size_t offset, DWORD value) {
|
||||
std::memcpy(bytes.data() + offset, &value, 4);
|
||||
}
|
||||
|
||||
std::map<DWORD, std::string> read_names(const char* path) {
|
||||
std::ifstream stream(path, std::ios::binary);
|
||||
if (!stream) throw std::runtime_error(std::string("cannot read ") + path);
|
||||
std::map<DWORD, std::string> names;
|
||||
std::string line;
|
||||
while (std::getline(stream, line)) {
|
||||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||
const std::size_t tab = line.find('\t');
|
||||
if (tab == std::string::npos || tab == 0) continue;
|
||||
try {
|
||||
std::size_t end = 0;
|
||||
const unsigned long value = std::stoul(line.substr(0, tab), &end);
|
||||
if (end != tab || value > UINT32_MAX) continue;
|
||||
const std::string name = line.substr(tab + 1);
|
||||
if (!name.empty() && name.find('\0') == std::string::npos)
|
||||
names[static_cast<DWORD>(value)] = name;
|
||||
} catch (const std::exception&) { /* header or malformed row */ }
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
template <class Record>
|
||||
std::vector<BYTE> patch(const char* source, const char* names_path,
|
||||
std::array<DWORD, 4> key, const char* magic, bool versioned) {
|
||||
const auto input = read_file(source);
|
||||
const std::size_t count_offset = versioned ? 12 : 4;
|
||||
const std::size_t payload_offset = versioned ? 20 : 12;
|
||||
if (input.size() < payload_offset || std::memcmp(input.data(), magic, 4) != 0)
|
||||
throw std::runtime_error("unexpected proto signature");
|
||||
if (versioned && (word(input, 4) != 1 || word(input, 8) != sizeof(Record)))
|
||||
throw std::runtime_error("unexpected item proto version or stride");
|
||||
const DWORD count = word(input, count_offset);
|
||||
const DWORD payload_size = word(input, count_offset + 4);
|
||||
if (payload_size > input.size() - payload_offset)
|
||||
throw std::runtime_error("truncated compressed proto");
|
||||
|
||||
CLZObject decoded;
|
||||
if (!CLZO::Instance().Decompress(decoded, input.data() + payload_offset, key.data()) ||
|
||||
decoded.GetSize() != std::size_t(count) * sizeof(Record))
|
||||
throw std::runtime_error("invalid proto payload size or key");
|
||||
std::vector<BYTE> plain(decoded.GetBuffer(), decoded.GetBuffer() + decoded.GetSize());
|
||||
const auto original = plain;
|
||||
const auto names = read_names(names_path);
|
||||
std::size_t translated = 0, missing = 0, too_long = 0;
|
||||
for (DWORD i = 0; i < count; ++i) {
|
||||
Record record;
|
||||
std::memcpy(&record, plain.data() + std::size_t(i) * sizeof(Record), sizeof(Record));
|
||||
const auto found = names.find(record.dwVnum);
|
||||
if (found == names.end()) { ++missing; continue; }
|
||||
if (found->second.size() >= sizeof(record.szLocaleName)) { ++too_long; continue; }
|
||||
std::memset(record.szLocaleName, 0, sizeof(record.szLocaleName));
|
||||
std::memcpy(record.szLocaleName, found->second.data(), found->second.size());
|
||||
std::memcpy(plain.data() + std::size_t(i) * sizeof(Record), &record, sizeof(Record));
|
||||
++translated;
|
||||
}
|
||||
// No item stats, VNUM, canonical name, or mob gameplay fields may change.
|
||||
for (DWORD i = 0; i < count; ++i) {
|
||||
const std::size_t start = std::size_t(i) * sizeof(Record);
|
||||
const std::size_t name_start = start + offsetof(Record, szLocaleName);
|
||||
const std::size_t name_end = name_start + sizeof(Record::szLocaleName);
|
||||
for (std::size_t byte = start; byte < start + sizeof(Record); ++byte) {
|
||||
if ((byte < name_start || byte >= name_end) && plain[byte] != original[byte])
|
||||
throw std::runtime_error("non-name proto data changed");
|
||||
}
|
||||
}
|
||||
|
||||
CLZObject encoded;
|
||||
if (!CLZO::Instance().CompressEncryptedMemory(encoded, plain.data(), plain.size(), key.data()))
|
||||
throw std::runtime_error("cannot encode proto");
|
||||
std::vector<BYTE> result(input.begin(), input.begin() + payload_offset);
|
||||
result.insert(result.end(), encoded.GetBuffer(), encoded.GetBuffer() + encoded.GetSize());
|
||||
put_word(result, count_offset + 4, encoded.GetSize());
|
||||
CLZObject verified;
|
||||
if (!CLZO::Instance().Decompress(verified, result.data() + payload_offset, key.data()) ||
|
||||
verified.GetSize() != plain.size() ||
|
||||
std::memcmp(verified.GetBuffer(), plain.data(), plain.size()) != 0)
|
||||
throw std::runtime_error("encoded proto failed round-trip verification");
|
||||
std::cout << magic << " count=" << count << " translated=" << translated
|
||||
<< " missing=" << missing << " too_long=" << too_long << '\n';
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 7) {
|
||||
std::cerr << "usage: locale_proto_patch item_proto mob_proto item_names_gbk mob_names_gbk out_item out_mob\n";
|
||||
return 2;
|
||||
}
|
||||
try {
|
||||
static CLZO lzo;
|
||||
const auto item = patch<Item>(argv[1], argv[3], item_key, "MIPX", true);
|
||||
const auto mob = patch<Mob>(argv[2], argv[4], mob_key, "MMPT", false);
|
||||
write_file(argv[5], item);
|
||||
write_file(argv[6], mob);
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "locale_proto_patch: " << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,26 @@ static void drawing_primitives()
|
||||
graphic.RenderCoolTimeBox(100, 100, 10, 1.0f);
|
||||
CHECK(commands.empty());
|
||||
|
||||
// A cooldown triangle crossing the viewport must have clipped vertices, not only a clipped bbox.
|
||||
UIRenderSetClip(95, 95, 10, 10);
|
||||
graphic.RenderCoolTimeBox(100, 100, 10, 0.25f);
|
||||
CHECK(!commands.empty());
|
||||
bool touched_clip_edge = false;
|
||||
for (const auto& command : commands)
|
||||
{
|
||||
CHECK(command.kind == UIRenderCommand::Bar && command.quad);
|
||||
for (int vertex = 0; vertex < 3; ++vertex)
|
||||
{
|
||||
CHECK(command.qx[vertex] >= 95 && command.qx[vertex] <= 105);
|
||||
CHECK(command.qy[vertex] >= 95 && command.qy[vertex] <= 105);
|
||||
touched_clip_edge |= near(command.qx[vertex], 95) || near(command.qx[vertex], 105) ||
|
||||
near(command.qy[vertex], 95) || near(command.qy[vertex], 105);
|
||||
}
|
||||
}
|
||||
CHECK(touched_clip_edge);
|
||||
UIRenderRestoreClip();
|
||||
UIRenderBeginFrame();
|
||||
|
||||
// CPythonGraphic::SetViewport also clips the UI commands to the viewport until RestoreViewport.
|
||||
graphic.SetViewport(10, 20, 30, 40);
|
||||
screen.RenderBar2d(0, 0, 5, 5);
|
||||
|
||||
Reference in New Issue
Block a user