Files
metin2-client/extension/src/python_host_node.cpp
T
shenleiandClaude Opus 5.5 afe63bfa61 2V0-g: port the .fnt font pipeline over a FreeType GDI emulation
CGraphicText, CGraphicFontTexture and CGraphicDib are the 40250 bodies
verbatim; CGraphicTextInstance is verbatim except that its D3D draws become
UI render commands (one image quad per glyph, Bar for cursor/underline).

- platform/Win32Gdi.cpp: the GDI subset those files call, over vendored
  FreeType 2.13.3, following GDI's LOGFONT cell height, win metrics and gasp
  rules; system font lookup with substitute and CJK fallback faces
- Win32Crt: MultiByteToWideChar / WideCharToMultiByte (UTF-8 and 1252 by
  hand, other code pages through iconv)
- CGraphicImageTexture memory textures: glyph pages reach Godot as
  "mem:<id>@<revision>"; Metin2PythonHost.memory_texture + Canvas cache
- Util.cpp code page / font face table, SetDefaultCodePage at boot,
  DefaultFont_* block, fnt resource type, GetMaxTextureWidth/Height
- TextTag, StringCodec, Arabic, Vietnamese copied from 40250
- tests: port.text (Tahoma 12 metrics, alignment, CP949), port.app_loop
  and python_host_test.gd now check glyph quads instead of text commands

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:17:25 +09:00

178 lines
6.8 KiB
C++

#include "python_host_node.h"
#include <godot_cpp/core/class_db.hpp>
#include "python_stdlib.h"
#ifdef MTGODOT_HAVE_PYTHON
// Standard types only — PythonBoot.h keeps <Python-2.7/*> and the Win32 shims out of this translation
// unit, which has to stay compilable next to godot-cpp.
#include "platform/ScriptLib/PythonBoot.h"
#include "platform/EterLib/UIRenderCommands.h"
#include <string>
#endif
#include <godot_cpp/classes/image.hpp>
#include <godot_cpp/variant/dictionary.hpp>
#include <godot_cpp/variant/packed_byte_array.hpp>
using namespace godot;
namespace mtgodot {
#ifdef MTGODOT_HAVE_PYTHON
String Metin2PythonHost::start(const String &stdlib_path) {
String path = stdlib_path;
if (path.is_empty()) {
path = Metin2Python::stdlib_path(false);
if (path.is_empty()) {
return String("python27.zip unavailable: ") + Metin2Python::last_error();
}
}
std::string error;
if (!PythonBoot::Start(path.utf8().get_data(), &error)) {
return String::utf8(error.c_str());
}
return String();
}
bool Metin2PythonHost::is_running() {
return PythonBoot::IsRunning();
}
String Metin2PythonHost::run_main_script(const String &command_line) {
std::string error;
if (!PythonBoot::RunMainScript(command_line.utf8().get_data(), &error)) {
return String::utf8(error.c_str());
}
return String();
}
String Metin2PythonHost::run_line(const String &source) {
std::string error;
if (!PythonBoot::RunLine(source.utf8().get_data(), &error)) {
return String::utf8(error.c_str());
}
return String();
}
bool Metin2PythonHost::is_app_looping() { return PythonBoot::IsAppLooping(); }
void Metin2PythonHost::set_ui_size(int width, int height) { PythonBoot::SetUISize(width, height); }
void Metin2PythonHost::ui_mouse_move(int x, int y) { PythonBoot::UIMouseMove(x, y); }
void Metin2PythonHost::ui_mouse_button(int button, bool pressed, int x, int y) {
PythonBoot::UIMouseButton(button, pressed, x, y);
}
void Metin2PythonHost::ui_key(int key, bool pressed) { PythonBoot::UIKey(key, pressed); }
void Metin2PythonHost::ui_update() { PythonBoot::UIUpdate(); }
Array Metin2PythonHost::ui_render_commands() {
PythonBoot::UIRender();
Array out;
for (const auto &command : UIRenderCommands()) {
Dictionary item;
item["kind"] = command.kind == UIRenderCommand::Bar ? "bar" :
(command.kind == UIRenderCommand::Line ? "line" :
(command.kind == UIRenderCommand::Image ? "image" : "text"));
item["x1"] = command.x1;
item["y1"] = command.y1;
item["x2"] = command.x2;
item["y2"] = command.y2;
item["argb"] = static_cast<int64_t>(command.argb);
item["clip_x1"] = command.clip_x1;
item["clip_y1"] = command.clip_y1;
item["clip_x2"] = command.clip_x2;
item["clip_y2"] = command.clip_y2;
if (command.kind == UIRenderCommand::Text || command.kind == UIRenderCommand::Image)
item["text"] = String::utf8(command.text.c_str());
if (command.quad) {
PackedVector2Array quad;
for (int i = 0; i < 4; ++i)
quad.push_back(Vector2(command.qx[i], command.qy[i]));
item["quad"] = quad;
item["uv"] = PackedVector2Array({Vector2(command.su, command.sv), Vector2(command.eu, command.sv),
Vector2(command.su, command.ev), Vector2(command.eu, command.ev)});
item["blend"] = command.blend;
}
out.push_back(item);
}
return out;
}
Ref<Image> Metin2PythonHost::memory_texture(const String &name, int64_t known_revision) {
UIMemoryTexture texture;
if (!UIRenderMemoryTexture(name.utf8().get_data(), &texture) || texture.width <= 0 || texture.height <= 0)
return Ref<Image>();
if (known_revision >= 0 && texture.revision == static_cast<std::uint32_t>(known_revision))
return Ref<Image>();
PackedByteArray rgba;
rgba.resize(static_cast<int64_t>(texture.argb.size()) * 4);
uint8_t *out = rgba.ptrw();
for (std::uint32_t argb : texture.argb) {
*out++ = static_cast<uint8_t>(argb >> 16);
*out++ = static_cast<uint8_t>(argb >> 8);
*out++ = static_cast<uint8_t>(argb);
*out++ = static_cast<uint8_t>(argb >> 24);
}
return Image::create_from_data(texture.width, texture.height, false, Image::FORMAT_RGBA8, rgba);
}
void Metin2PythonHost::stop() {
PythonBoot::Stop();
}
#else
// Built without -DMTGODOT_EMBED_PYTHON=ON: the class is still registered so GDScript can tell "no
// interpreter in this build" apart from "the interpreter failed to start".
String Metin2PythonHost::start(const String &) {
return String("this build has no embedded Python (-DMTGODOT_EMBED_PYTHON=ON)");
}
bool Metin2PythonHost::is_running() {
return false;
}
String Metin2PythonHost::run_main_script(const String &) {
return String("this build has no embedded Python (-DMTGODOT_EMBED_PYTHON=ON)");
}
String Metin2PythonHost::run_line(const String &) {
return String("this build has no embedded Python (-DMTGODOT_EMBED_PYTHON=ON)");
}
bool Metin2PythonHost::is_app_looping() { return false; }
void Metin2PythonHost::set_ui_size(int, int) {}
void Metin2PythonHost::ui_mouse_move(int, int) {}
void Metin2PythonHost::ui_mouse_button(int, bool, int, int) {}
void Metin2PythonHost::ui_key(int, bool) {}
void Metin2PythonHost::ui_update() {}
Array Metin2PythonHost::ui_render_commands() { return Array(); }
Ref<Image> Metin2PythonHost::memory_texture(const String &, int64_t) { return Ref<Image>(); }
void Metin2PythonHost::stop() {
}
#endif
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("run_main_script", "command_line"),
&Metin2PythonHost::run_main_script, DEFVAL(String()));
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("run_line", "source"), &Metin2PythonHost::run_line);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("is_app_looping"), &Metin2PythonHost::is_app_looping);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("set_ui_size", "width", "height"), &Metin2PythonHost::set_ui_size);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_mouse_move", "x", "y"), &Metin2PythonHost::ui_mouse_move);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_mouse_button", "button", "pressed", "x", "y"), &Metin2PythonHost::ui_mouse_button);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_key", "key", "pressed"), &Metin2PythonHost::ui_key);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_update"), &Metin2PythonHost::ui_update);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("ui_render_commands"), &Metin2PythonHost::ui_render_commands);
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("memory_texture", "name", "known_revision"),
&Metin2PythonHost::memory_texture, DEFVAL(-1));
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("stop"), &Metin2PythonHost::stop);
}
} // namespace mtgodot