Compare commits
1
Commits
feature/vip-card
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84404cf1f1 |
@@ -115,7 +115,7 @@
|
||||
|
||||
2026-10-15 后续批次:阶段 0 的只读能力协商、阶段 2/3 的掉落/来源/宝箱查询、阶段 4 属性面板已实现并在隔离环境测试;未部署 203,待联机与真机验收。范围和缺口见 AVA2-READONLY-FEATURES.md。
|
||||
|
||||
2026-10-15 再续:龙穴/周任务信息与提醒、共享真实掉落开关的活动日历已实现并在隔离环境验证。副本仅首批龙穴;活动仅月光宝箱/圣诞袜,生产未部署,无生产排期。自动洗练/锁定、战令、模型预览、批量开箱仍待实现。见 AVA2-DUNGEON-CALENDAR.md。
|
||||
2026-10-15 再续:龙穴/周任务信息与提醒、共享真实掉落开关的活动日历已实现并在隔离环境验证。副本仅首批龙穴;活动仅月光宝箱/圣诞袜,生产未部署,无生产排期。自动洗练/锁定、战令、模型预览、批量开箱仍待实现(2026-10-16:模型预览已实现,见 FEATURE-MODEL-PREVIEW.md)。见 AVA2-DUNGEON-CALENDAR.md。
|
||||
|
||||
2026-10-15 自动洗练 v1(阶段 6,不含属性锁定):服务端 mt_switchbot 逐次调用原 UseItem 洗练路径,客户端 mt_switchbot 窗口;隔离环境编译与桩测试通过,macOS 客户端已在 203 测试核心联机验收;未部署 203 正式环境、未做 Android 真机测试。属性锁定仍待存储设计。见 AVA2-SWITCHBOT.md。
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Model preview
|
||||
|
||||
Ported from AVA2's render-target window (`EterLib/CRenderTarget*`, `GrpRenderTargetTexture.*`, `UserInterface/CPythonRenderTargetModule.cpp`, `root/uirendertarget.py`). The preview shows:
|
||||
- a wearable item on a character of every race that may wear it;
|
||||
- a monster, with its motions.
|
||||
|
||||
It is client only; the server is not involved.
|
||||
|
||||
## Entry points
|
||||
|
||||
| Where | How |
|
||||
|---|---|
|
||||
| Inventory | hold X and left-click the item |
|
||||
| NPC shop | hold X and left-click the item |
|
||||
| Wiki detail | the "模型预览" line, on weapon / armor / costume rows that have a model, and on every monster |
|
||||
|
||||
Without X the original click path runs unchanged. The inventory wrapper chains onto `mt_mobile_inventory`'s, because `mt_preview` is installed after it. There is no touch entry other than the wiki.
|
||||
|
||||
## Window (`ScriptLib/ModelPreviewScript.inc`, module `mt_preview`)
|
||||
|
||||
### Item mode
|
||||
- `ItemSpec(vnum)` accepts:
|
||||
- a weapon other than an arrow → `SetWeapon`;
|
||||
- a body armor or a body costume → `SetArmor`;
|
||||
- a hair costume → `SetHair`, which reads value3.
|
||||
- The races are those not excluded by `ITEM_ANTIFLAG_{MALE,FEMALE,WARRIOR,ASSASSIN,SURA,SHAMAN}`, ordered warrior, assassin, sura, shaman, with male first. The window starts on the player's own race if it is allowed.
|
||||
- `<` / `>` step through the allowed races.
|
||||
|
||||
### Mob mode
|
||||
- `SelectModel(vnum)`.
|
||||
- Motion buttons: 待机 1, 攻击 13, 受击 5, 死亡 11, 技能1–4 33–36 (`InterceptOnceMotion`).
|
||||
|
||||
### Controls
|
||||
- Dragging with the left button turns the camera: absolute yaw/pitch from the press point, pitch clamped to ±60°.
|
||||
- 放大 / 缩小 change the zoom in 5 % steps, clamped to −8..+12. Windows have no wheel event.
|
||||
- 自动旋转 spins the model.
|
||||
- 复位 rebuilds the model, which also resets the camera.
|
||||
|
||||
### Lifetime
|
||||
- Render target index 1 is created at 300×330 when the window opens.
|
||||
- It is removed on close or Esc, and on every re-open before it is created again. `GameWindow.Close` also calls `renderTarget.Destroy()`.
|
||||
- The host frees the GPU image 120 frames after its last use.
|
||||
|
||||
## Native side
|
||||
|
||||
- `renderTarget` Python module (`port/UserInterface/PythonRenderTargetModule.cpp`) → `CRenderTargetManager` → `CRenderTarget`, which owns a `CInstanceBase` and a `CGraphicRenderTargetTexture`.
|
||||
- `CPythonApplication` calls `DeformModels` / `UpdateModels` / `RenderModels` around the character manager.
|
||||
|
||||
### Port differences from AVA2
|
||||
- Player races (0..7) are created as `TYPE_PC`. 40250 reserves the armor/weapon/hair parts only for PCs, so AVA2's `TYPE_NPC` crashed on the first `ChangeArmor`.
|
||||
- The model sits at the world origin, where culling hides it. Instead of AVA2's `SetAlwaysRender`, it is shown again before `Deform` and `Render`. Its text tail is detached.
|
||||
- The background quad is drawn inside the render target, in `RenderModel`. `RenderBackgrounds` is a no-op.
|
||||
- Wiki affects are not ported.
|
||||
- The texture has 1 mip level.
|
||||
|
||||
### Host (Vulkan)
|
||||
- A colour render target is `D3DFMT_X8R8G8B8` / `A8R8G8B8` with `D3DUSAGE_RENDERTARGET`, named `rt:<id>:<w>x<h>:c`.
|
||||
- It gets its own RGBA8 offscreen pass (pipeline key bits 29–30: 0 none, 1 shadow, 2 colour) and is cleared to black.
|
||||
- Shadow targets keep the previous path.
|
||||
- Side effect: SnowEnvironment's blur targets now take the colour path. Snow blur is off by default.
|
||||
|
||||
## Verification
|
||||
|
||||
- `tests/wiki/test_model_preview.py` covers:
|
||||
- `ItemSpec` races and slots;
|
||||
- race stepping and fallback;
|
||||
- motions and zoom clamps;
|
||||
- drag;
|
||||
- reset;
|
||||
- the view drawing the target;
|
||||
- no live target after close, Esc, or 5 re-opens;
|
||||
- the inventory and shop wrappers;
|
||||
- wiki links and module order.
|
||||
- ctest 49/49.
|
||||
- Live, channel5 13040, macOS client, 2026-10-16:
|
||||
- armor 11299 on a warrior;
|
||||
- sword 19 after a 90°/20° turn;
|
||||
- wild dog 101 with the attack motion;
|
||||
- boss 691 zoomed in with spin.
|
||||
- The main scene was unchanged.
|
||||
- 120 open/close cycles (30 paced, plus 60 in one frame) did not grow RSS: 668 MB before, 593 MB after.
|
||||
+10
-8
@@ -173,7 +173,7 @@ void VulkanWindow::prepare_draws(const std::vector<Render3DDraw>& draws, const T
|
||||
RenderTarget* target = nullptr;
|
||||
if (draw.render_target) {
|
||||
target = get_render_target(
|
||||
Render3DRenderTargetName(draw.render_target, draw.target_width, draw.target_height));
|
||||
Render3DRenderTargetName(draw.render_target, draw.target_width, draw.target_height, draw.target_color));
|
||||
if (!target) continue;
|
||||
}
|
||||
if (draw.clear_flags && target) {
|
||||
@@ -257,7 +257,8 @@ void VulkanWindow::prepare_draws(const std::vector<Render3DDraw>& draws, const T
|
||||
}
|
||||
}
|
||||
const auto pipeline = get_pipeline(cull, depth, blend, draw.lines,
|
||||
static_cast<std::uint8_t>(draw.z_func), target != nullptr, &draw);
|
||||
static_cast<std::uint8_t>(draw.z_func),
|
||||
std::uint8_t(target ? (target->color ? 2 : 1) : 0), &draw);
|
||||
// D3DTOP_DISABLE (1) on stage 0 or 1 ends the cascade before stage 1 samples.
|
||||
const bool bind_tex1 = !draw.texture1.empty() && draw.color_op[0] > 1 && draw.color_op[1] > 1;
|
||||
const auto descriptor = get_texture_descriptor(
|
||||
@@ -265,8 +266,9 @@ void VulkanWindow::prepare_draws(const std::vector<Render3DDraw>& draws, const T
|
||||
|
||||
auto constants = make_push_constants(draw, skin_offset_encoded);
|
||||
if (draw.pretransformed) {
|
||||
const float screen_width = float(logical_width());
|
||||
const float screen_height = float(logical_height());
|
||||
// PORT: a pretransformed draw into a render target is in the target's pixels.
|
||||
const float screen_width = target ? float(target->width) : float(logical_width());
|
||||
const float screen_height = target ? float(target->height) : float(logical_height());
|
||||
// Screen pixels (y down) to D3D NDC (y up); the shader's Y flip then maps to Vulkan.
|
||||
constants.mvp = {2.0f / screen_width, 0, 0, 0,
|
||||
0, -2.0f / screen_height, 0, 0,
|
||||
@@ -448,7 +450,7 @@ void VulkanWindow::record_passes(std::uint32_t image_index, const FrameDraws& fr
|
||||
for (std::size_t i = 0; i < frame.offscreen_draws.size();) {
|
||||
RenderTarget* target = frame.offscreen_draws[i].target;
|
||||
VkRenderPassBeginInfo offscreen_begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
||||
offscreen_begin.renderPass = offscreen_pass_;
|
||||
offscreen_begin.renderPass = target->color ? offscreen_color_pass_ : offscreen_pass_;
|
||||
offscreen_begin.framebuffer = target->framebuffer;
|
||||
offscreen_begin.renderArea.extent = {target->width, target->height};
|
||||
vkCmdBeginRenderPass(f_->command, &offscreen_begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
@@ -559,10 +561,10 @@ VulkanWindow::Readback VulkanWindow::record_readbacks(std::uint32_t image_index)
|
||||
const char* dump_rt = std::getenv("MT_DUMP_RT");
|
||||
if (readback.bmp && dump_rt && *dump_rt) {
|
||||
readback.dump_rt = dump_rt;
|
||||
const std::uint32_t texel = offscreen_format_ == VK_FORMAT_R5G6B5_UNORM_PACK16 ? 2 : 4;
|
||||
for (auto& entry : render_targets_) {
|
||||
RenderTarget& rt = entry.second;
|
||||
Readback::RtDump dump{VK_NULL_HANDLE, VK_NULL_HANDLE, rt.width, rt.height};
|
||||
const std::uint32_t texel = !rt.color && offscreen_format_ == VK_FORMAT_R5G6B5_UNORM_PACK16 ? 2 : 4;
|
||||
Readback::RtDump dump{VK_NULL_HANDLE, VK_NULL_HANDLE, rt.width, rt.height, texel};
|
||||
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||
info.size = VkDeviceSize(rt.width) * rt.height * texel; info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
check(vkCreateBuffer(device_, &info, nullptr, &dump.buffer), "vkCreateBuffer rt dump");
|
||||
@@ -615,9 +617,9 @@ void VulkanWindow::finish_readbacks(Readback& readback) {
|
||||
for (auto& sink : readback.sinks) sink(bgra.data(), w, h);
|
||||
}
|
||||
vkUnmapMemory(device_, readback.memory);
|
||||
const std::uint32_t texel = offscreen_format_ == VK_FORMAT_R5G6B5_UNORM_PACK16 ? 2 : 4;
|
||||
for (std::size_t n = 0; n < readback.rt_dumps.size(); ++n) {
|
||||
const auto& dump = readback.rt_dumps[n];
|
||||
const std::uint32_t texel = dump.texel;
|
||||
void* data = nullptr;
|
||||
check(vkMapMemory(device_, dump.memory, 0, VK_WHOLE_SIZE, 0, &data), "vkMapMemory rt dump");
|
||||
const auto* bytes = static_cast<const std::uint8_t*>(data);
|
||||
|
||||
@@ -104,8 +104,13 @@ void VulkanWindow::create_offscreen_pass() {
|
||||
VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT;
|
||||
offscreen_format_ = (properties.optimalTilingFeatures & wanted) == wanted
|
||||
? VK_FORMAT_R5G6B5_UNORM_PACK16 : VK_FORMAT_R8G8B8A8_UNORM;
|
||||
offscreen_pass_ = make_offscreen_pass(offscreen_format_);
|
||||
offscreen_color_pass_ = make_offscreen_pass(VK_FORMAT_R8G8B8A8_UNORM);
|
||||
}
|
||||
|
||||
VkRenderPass VulkanWindow::make_offscreen_pass(VkFormat format) {
|
||||
VkAttachmentDescription attachments[2]{};
|
||||
attachments[0].format = offscreen_format_; attachments[0].samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
attachments[0].format = format; attachments[0].samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||
@@ -146,15 +151,17 @@ void VulkanWindow::create_offscreen_pass() {
|
||||
pass_info.attachmentCount = 2; pass_info.pAttachments = attachments;
|
||||
pass_info.subpassCount = 1; pass_info.pSubpasses = &subpass;
|
||||
pass_info.dependencyCount = 2; pass_info.pDependencies = dependencies;
|
||||
check(vkCreateRenderPass(device_, &pass_info, nullptr, &offscreen_pass_), "vkCreateRenderPass offscreen");
|
||||
VkRenderPass pass = VK_NULL_HANDLE;
|
||||
check(vkCreateRenderPass(device_, &pass_info, nullptr, &pass), "vkCreateRenderPass offscreen");
|
||||
return pass;
|
||||
}
|
||||
|
||||
VkPipeline VulkanWindow::get_pipeline(std::uint8_t cull, std::uint8_t depth, std::uint8_t blend,
|
||||
bool lines, std::uint8_t z_func, bool offscreen, const Render3DDraw* draw) {
|
||||
bool lines, std::uint8_t z_func, std::uint8_t offscreen, const Render3DDraw* draw) {
|
||||
std::array<std::uint32_t, 17> key{};
|
||||
key[0] = std::uint32_t(cull) | (std::uint32_t(depth) << 8) |
|
||||
(std::uint32_t(blend) << 16) | (lines ? (1u << 24) : 0u) |
|
||||
(std::uint32_t(z_func & 0xfu) << 25) | (offscreen ? (1u << 29) : 0u);
|
||||
(std::uint32_t(z_func & 0xfu) << 25) | (std::uint32_t(offscreen & 3u) << 29);
|
||||
static const bool specialize = std::getenv("MT_GENERIC_SHADERS") == nullptr;
|
||||
key[1] = specialize ? 1u : 0u;
|
||||
if (specialize && draw) {
|
||||
@@ -296,7 +303,7 @@ VkPipeline VulkanWindow::get_pipeline(std::uint8_t cull, std::uint8_t depth, std
|
||||
pipeline_info.pMultisampleState = &multisample; pipeline_info.pDepthStencilState = &depth_stencil;
|
||||
pipeline_info.pColorBlendState = &blending;
|
||||
pipeline_info.pDynamicState = &dynamic_info;
|
||||
pipeline_info.layout = layout_; pipeline_info.renderPass = offscreen ? offscreen_pass_ : pass_;
|
||||
pipeline_info.layout = layout_; pipeline_info.renderPass = offscreen == 2 ? offscreen_color_pass_ : offscreen ? offscreen_pass_ : pass_;
|
||||
VkPipeline handle = VK_NULL_HANDLE;
|
||||
check(vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &handle), "vkCreateGraphicsPipelines");
|
||||
pipelines_.emplace(key, handle);
|
||||
|
||||
@@ -270,15 +270,23 @@ void VulkanWindow::ensure_ui_capacity(std::size_t command_count) {
|
||||
}
|
||||
|
||||
VulkanWindow::RenderTarget* VulkanWindow::get_render_target(const std::string& name) {
|
||||
if (auto it = render_targets_.find(name); it != render_targets_.end()) return &it->second;
|
||||
if (auto it = render_targets_.find(name); it != render_targets_.end()) {
|
||||
it->second.last_used_frame = frame_number_;
|
||||
return &it->second;
|
||||
}
|
||||
unsigned id = 0, w = 0, h = 0;
|
||||
if (std::sscanf(name.c_str(), "rt:%u:%ux%u", &id, &w, &h) != 3 || !w || !h || w > 4096 || h > 4096)
|
||||
int end = 0;
|
||||
if (std::sscanf(name.c_str(), "rt:%u:%ux%u%n", &id, &w, &h, &end) != 3 || !w || !h || w > 4096 || h > 4096)
|
||||
return nullptr;
|
||||
const bool color = name.compare(std::size_t(end), std::string::npos, ":c") == 0;
|
||||
if (!color && name[std::size_t(end)] != '\0') return nullptr;
|
||||
RenderTarget& target = render_targets_[name];
|
||||
target.width = w;
|
||||
target.height = h;
|
||||
target.color = color;
|
||||
target.last_used_frame = frame_number_;
|
||||
auto make_image = [&](VkFormat format, VkImageUsageFlags usage, VkImageAspectFlags aspect,
|
||||
VkImage& image, VkDeviceMemory& memory, VkImageView& view) {
|
||||
VkImage& image, VkDeviceMemory& memory, VkImageView& view, bool opaque = false) {
|
||||
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
info.imageType = VK_IMAGE_TYPE_2D;
|
||||
info.format = format;
|
||||
@@ -299,18 +307,20 @@ VulkanWindow::RenderTarget* VulkanWindow::get_render_target(const std::string& n
|
||||
VkImageViewCreateInfo view_info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
view_info.image = image; view_info.viewType = VK_IMAGE_VIEW_TYPE_2D; view_info.format = format;
|
||||
view_info.subresourceRange = {aspect, 0, 1, 0, 1};
|
||||
// A colour target is D3DFMT_X8R8G8B8: sampled alpha is always 1.
|
||||
if (opaque) view_info.components.a = VK_COMPONENT_SWIZZLE_ONE;
|
||||
check(vkCreateImageView(device_, &view_info, nullptr, &view), "vkCreateImageView render target");
|
||||
};
|
||||
make_image(offscreen_format_,
|
||||
make_image(color ? VK_FORMAT_R8G8B8A8_UNORM : offscreen_format_,
|
||||
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
|
||||
VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||||
VK_IMAGE_ASPECT_COLOR_BIT, target.texture.image, target.texture.memory, target.texture.view);
|
||||
VK_IMAGE_ASPECT_COLOR_BIT, target.texture.image, target.texture.memory, target.texture.view, color);
|
||||
make_image(VK_FORMAT_D32_SFLOAT,
|
||||
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
|
||||
VK_IMAGE_ASPECT_DEPTH_BIT, target.depth_image, target.depth_memory, target.depth_view);
|
||||
const VkImageView views[2] = {target.texture.view, target.depth_view};
|
||||
VkFramebufferCreateInfo fb{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
||||
fb.renderPass = offscreen_pass_;
|
||||
fb.renderPass = color ? offscreen_color_pass_ : offscreen_pass_;
|
||||
fb.attachmentCount = 2; fb.pAttachments = views;
|
||||
fb.width = w; fb.height = h; fb.layers = 1;
|
||||
check(vkCreateFramebuffer(device_, &fb, nullptr, &target.framebuffer), "vkCreateFramebuffer render target");
|
||||
@@ -343,7 +353,8 @@ void VulkanWindow::initialize_render_target(RenderTarget& target) {
|
||||
barriers[1].subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
|
||||
vkCmdPipelineBarrier(f_->command, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0, 0, nullptr, 0, nullptr, 2, barriers);
|
||||
const VkClearColorValue white{{1.0f, 1.0f, 1.0f, 1.0f}};
|
||||
const float shade = target.color ? 0.0f : 1.0f;
|
||||
const VkClearColorValue white{{shade, shade, shade, 1.0f}};
|
||||
const VkClearDepthStencilValue far{1.0f, 0};
|
||||
vkCmdClearColorImage(f_->command, target.texture.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &white, 1,
|
||||
&barriers[0].subresourceRange);
|
||||
|
||||
@@ -181,7 +181,7 @@ VulkanWindow::PreparedDraw VulkanWindow::scene_composite_draw() {
|
||||
for (int i = 0; i < 16; ++i) m[i] = i % 5 == 0 ? 1.0f : 0.0f;
|
||||
PreparedDraw composite{};
|
||||
composite.geometry = &scene_quad_;
|
||||
composite.pipeline = get_pipeline(0, 0, 0, false, 4, false, &quad);
|
||||
composite.pipeline = get_pipeline(0, 0, 0, false, 4, 0, &quad);
|
||||
composite.descriptor = scene_texture_.descriptor;
|
||||
composite.constants = make_push_constants(quad, 0.0f);
|
||||
// Unit square (y down) to D3D NDC (y up), as the pretransformed draws map screen pixels.
|
||||
|
||||
@@ -430,6 +430,19 @@ void VulkanWindow::prune_textures() {
|
||||
it = textures_.erase(it);
|
||||
} else ++it;
|
||||
}
|
||||
// PORT: render targets too (a closed model preview); a later use recreates and clears one.
|
||||
for (auto it = render_targets_.begin(); it != render_targets_.end();) {
|
||||
if (frame_number_ - it->second.last_used_frame > static_retention_frames) {
|
||||
for (auto pair = paired_descriptors_.begin(); pair != paired_descriptors_.end();) {
|
||||
if (pair->first.find("\n" + it->first + "\n") != std::string::npos) {
|
||||
vkFreeDescriptorSets(device_, descriptor_pool_, 1, &pair->second.set);
|
||||
pair = paired_descriptors_.erase(pair);
|
||||
} else ++pair;
|
||||
}
|
||||
release_render_target(it->second);
|
||||
it = render_targets_.erase(it);
|
||||
} else ++it;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mt_host
|
||||
|
||||
@@ -125,6 +125,7 @@ VulkanWindow::~VulkanWindow() {
|
||||
for (auto& entry : textures_) release_texture(entry.second);
|
||||
for (auto& entry : render_targets_) release_render_target(entry.second);
|
||||
if (offscreen_pass_) vkDestroyRenderPass(device_, offscreen_pass_, nullptr);
|
||||
if (offscreen_color_pass_) vkDestroyRenderPass(device_, offscreen_color_pass_, nullptr);
|
||||
destroy_scene_target();
|
||||
release_geometry(scene_quad_);
|
||||
if (scene_pass_) vkDestroyRenderPass(device_, scene_pass_, nullptr);
|
||||
|
||||
@@ -56,8 +56,9 @@ class VulkanWindow {
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
std::uint64_t last_used_frame = 0;
|
||||
};
|
||||
// An offscreen D3D render-target texture (the 40250 character shadow map, "rt:<id>:<w>x<h>"):
|
||||
// drawn by the frame's offscreen pass ahead of the back-buffer pass and sampled by that pass.
|
||||
// An offscreen D3D render-target texture (the 40250 character shadow map, "rt:<id>:<w>x<h>", or a
|
||||
// colour target such as the model preview, "rt:<id>:<w>x<h>:c"): drawn by the frame's offscreen pass
|
||||
// ahead of the back-buffer pass and sampled by that pass.
|
||||
// Between frames the colour image stays SHADER_READ_ONLY_OPTIMAL and the depth image
|
||||
// DEPTH_STENCIL_ATTACHMENT_OPTIMAL; the render pass keeps (LOAD/STORE) both, like a D3D surface.
|
||||
struct RenderTarget {
|
||||
@@ -67,7 +68,9 @@ class VulkanWindow {
|
||||
VkImageView depth_view = VK_NULL_HANDLE;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
std::uint32_t width = 0, height = 0;
|
||||
bool initialized = false; // cleared to white / depth 1 by the first frame that uses it
|
||||
bool initialized = false; // cleared (shadow white, colour black) / depth 1 by the first frame that uses it
|
||||
bool color = false; // RGBA8 drawn with offscreen_color_pass_; otherwise offscreen_format_
|
||||
std::uint64_t last_used_frame = 0; // pruned with the textures once unused
|
||||
};
|
||||
struct PreparedDraw {
|
||||
Geometry* geometry = nullptr;
|
||||
@@ -223,7 +226,7 @@ private:
|
||||
};
|
||||
// Host copies of the swapchain image (--screenshot-out) and, with MT_DUMP_RT, of every render target.
|
||||
struct Readback {
|
||||
struct RtDump { VkBuffer buffer; VkDeviceMemory memory; std::uint32_t w, h; };
|
||||
struct RtDump { VkBuffer buffer; VkDeviceMemory memory; std::uint32_t w, h, texel; };
|
||||
bool screenshot = false; // the swapchain image is copied (a BMP or a frame sink wants it)
|
||||
bool bmp = false; // --screenshot-out
|
||||
std::vector<FrameSink> sinks;
|
||||
@@ -297,7 +300,9 @@ private:
|
||||
|
||||
// The render pass of offscreen render-target textures: colour (R5G6B5 like the D3D surface when
|
||||
// the device can render to it) + D32 depth, both loaded and stored, colour left shader-readable.
|
||||
// PORT: plus offscreen_color_pass_, the same pass over RGBA8 for colour targets.
|
||||
void create_offscreen_pass();
|
||||
VkRenderPass make_offscreen_pass(VkFormat format);
|
||||
|
||||
// The reduced-resolution scene pass: pass_'s attachments (so pass_ pipelines draw in it) at
|
||||
// scene_extent_, resolving into the sampled scene_texture_. Unsupported when the swapchain
|
||||
@@ -322,9 +327,10 @@ private:
|
||||
// the layouts offscreen_pass_ expects. Recorded outside any render pass.
|
||||
void initialize_render_target(RenderTarget& target);
|
||||
|
||||
// offscreen: for offscreen_pass_ (single-sampled render-target texture) instead of pass_.
|
||||
// offscreen: 1 for offscreen_pass_, 2 for offscreen_color_pass_ (single-sampled render-target
|
||||
// texture) instead of pass_.
|
||||
VkPipeline get_pipeline(std::uint8_t cull, std::uint8_t depth, std::uint8_t blend,
|
||||
bool lines = false, std::uint8_t z_func = 4, bool offscreen = false,
|
||||
bool lines = false, std::uint8_t z_func = 4, std::uint8_t offscreen = 0,
|
||||
const Render3DDraw* draw = nullptr);
|
||||
|
||||
VkDescriptorSet allocate_texture_descriptor_set(VkImageView view0, VkImageView view1,
|
||||
@@ -478,6 +484,7 @@ private:
|
||||
std::unordered_map<std::string, PairedDescriptor> paired_descriptors_;
|
||||
std::unordered_map<std::string, RenderTarget> render_targets_;
|
||||
VkRenderPass offscreen_pass_ = VK_NULL_HANDLE;
|
||||
VkRenderPass offscreen_color_pass_ = VK_NULL_HANDLE;
|
||||
float render_scale_ = 1.0f;
|
||||
bool scene_supported_ = false;
|
||||
VkRenderPass scene_pass_ = VK_NULL_HANDLE;
|
||||
|
||||
@@ -192,6 +192,13 @@ HRESULT CpuSurface::LockRect(D3DLOCKED_RECT* locked, const RECT* rect, DWORD fla
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// PORT: a 32-bit render-target texture (the AVA2 model preview's CGraphicRenderTargetTexture), drawn on the
|
||||
// GPU with full state; the R5G6B5 ones are the character shadow map.
|
||||
bool is_color_target(const D3DSURFACE_DESC& desc)
|
||||
{
|
||||
return (desc.Usage & D3DUSAGE_RENDERTARGET) && (desc.Format == D3DFMT_X8R8G8B8 || desc.Format == D3DFMT_A8R8G8B8);
|
||||
}
|
||||
|
||||
D3DSURFACE_DESC surface_desc(UINT width, UINT height, D3DFORMAT format, DWORD usage, D3DPOOL pool)
|
||||
{
|
||||
D3DSURFACE_DESC desc = {};
|
||||
@@ -454,7 +461,10 @@ public:
|
||||
return S_OK;
|
||||
}
|
||||
auto* surface = static_cast<CpuSurface*>(m_renderTarget);
|
||||
if (!surface->parent || surface->level != 0 || surface->desc.Format != D3DFMT_R5G6B5)
|
||||
const bool colorTarget = surface->parent && surface->level == 0 && is_color_target(surface->desc);
|
||||
if (colorTarget && !g_gpu_render_targets)
|
||||
return S_OK;
|
||||
if (!colorTarget && (!surface->parent || surface->level != 0 || surface->desc.Format != D3DFMT_R5G6B5))
|
||||
return S_OK;
|
||||
if (g_gpu_render_targets)
|
||||
{
|
||||
@@ -463,6 +473,7 @@ public:
|
||||
clear.render_target = surface->parent->id;
|
||||
clear.target_width = surface->desc.Width;
|
||||
clear.target_height = surface->desc.Height;
|
||||
clear.target_color = colorTarget;
|
||||
clear.clear_flags = flags & (D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER);
|
||||
clear.clear_color = color;
|
||||
clear.clear_z = depth;
|
||||
@@ -1140,11 +1151,30 @@ private:
|
||||
if (m_renderTarget == m_backBuffer)
|
||||
Render3DAdd(std::move(draw));
|
||||
else if (g_gpu_render_targets)
|
||||
record_shadow(std::move(draw));
|
||||
{
|
||||
auto* surface = static_cast<CpuSurface*>(m_renderTarget);
|
||||
if (surface->parent && surface->level == 0 && is_color_target(surface->desc))
|
||||
record_color(std::move(draw), *surface);
|
||||
else
|
||||
record_shadow(std::move(draw));
|
||||
}
|
||||
else
|
||||
rasterize_shadow(draw);
|
||||
}
|
||||
|
||||
// PORT: a draw into a colour render target keeps every state the back buffer would get.
|
||||
void record_color(Render3DDraw draw, const CpuSurface& surface)
|
||||
{
|
||||
if (!draw.shared_geometry && draw.indices.size() < (draw.lines ? 2u : 3u))
|
||||
return;
|
||||
draw.render_target = surface.parent->id;
|
||||
draw.target_width = surface.desc.Width;
|
||||
draw.target_height = surface.desc.Height;
|
||||
draw.target_color = true;
|
||||
draw.ui_offset_x = 0;
|
||||
Render3DAdd(std::move(draw));
|
||||
}
|
||||
|
||||
// GPU counterpart of rasterize_shadow: the draw goes to the renderer's offscreen pass with the
|
||||
// states rasterize_shadow implements -- a flat D3DRS_TEXTUREFACTOR fill, no culling, no texture,
|
||||
// light, fog or blending, depth test LESSEQUAL with depth writes -- so both paths draw the same map.
|
||||
@@ -1257,6 +1287,8 @@ std::string MtCpuTextureNameFromHandle(const IDirect3DBaseTexture8* handle)
|
||||
const D3DSURFACE_DESC& desc = entry.second->levels[0].desc;
|
||||
if (g_gpu_render_targets && (desc.Usage & D3DUSAGE_RENDERTARGET) && desc.Format == D3DFMT_R5G6B5)
|
||||
return Render3DRenderTargetName(entry.first, desc.Width, desc.Height);
|
||||
if (g_gpu_render_targets && is_color_target(desc))
|
||||
return Render3DRenderTargetName(entry.first, desc.Width, desc.Height, true);
|
||||
if (entry.second == handle && !entry.second->levels.empty())
|
||||
return "mem:cpu_" + std::to_string(entry.first) + "@" + std::to_string(entry.second->revision);
|
||||
}
|
||||
@@ -1406,9 +1438,9 @@ bool IsNativeTerrainRenderEnabled()
|
||||
void SetGpuRenderTargetsEnabled(bool enabled) { g_gpu_render_targets = enabled; }
|
||||
bool IsGpuRenderTargetsEnabled() { return g_gpu_render_targets; }
|
||||
|
||||
std::string Render3DRenderTargetName(std::uint32_t id, std::uint32_t width, std::uint32_t height)
|
||||
std::string Render3DRenderTargetName(std::uint32_t id, std::uint32_t width, std::uint32_t height, bool color)
|
||||
{
|
||||
return "rt:" + std::to_string(id) + ":" + std::to_string(width) + "x" + std::to_string(height);
|
||||
return "rt:" + std::to_string(id) + ":" + std::to_string(width) + "x" + std::to_string(height) + (color ? ":c" : "");
|
||||
}
|
||||
|
||||
void Render3DBeginFrame()
|
||||
|
||||
@@ -38,6 +38,9 @@ struct Render3DDraw {
|
||||
// viewport is then in the target's pixels. Only emitted while GPU render targets are enabled.
|
||||
std::uint32_t render_target = 0;
|
||||
std::uint32_t target_width = 0, target_height = 0;
|
||||
// PORT: an X8R8G8B8 / A8R8G8B8 colour target (the AVA2 model preview, CRenderTarget) drawn with the
|
||||
// draw's own states, named with Render3DRenderTargetName(..., true); otherwise the R5G6B5 shadow map.
|
||||
bool target_color = false;
|
||||
|
||||
// Stage 0/1 textures, named like UIRenderTextureName: the pack path of a file texture or
|
||||
// "mem:<id>@<revision>"; empty when the stage has no texture.
|
||||
@@ -143,7 +146,8 @@ bool LookupGpuSkinSubrange(const void* vertex_buffer_base, std::uint32_t stride,
|
||||
// default (the Godot consumer has no offscreen pass); the native Vulkan renderer turns it on.
|
||||
void SetGpuRenderTargetsEnabled(bool enabled);
|
||||
bool IsGpuRenderTargetsEnabled();
|
||||
std::string Render3DRenderTargetName(std::uint32_t id, std::uint32_t width, std::uint32_t height);
|
||||
// A colour target's name ends in ":c".
|
||||
std::string Render3DRenderTargetName(std::uint32_t id, std::uint32_t width, std::uint32_t height, bool color = false);
|
||||
|
||||
void Render3DBeginFrame();
|
||||
void Render3DAdd(Render3DDraw draw);
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
// PORT: model preview, from AVA2's render-target window (uirendertarget.py). A wearable item is shown on a
|
||||
// character of every race that may wear it, a monster with its motions; drag rotates, buttons zoom. Entry points:
|
||||
// X + click on an inventory or shop item, and a "模型预览" line in the wiki detail. The native side is the
|
||||
// renderTarget module (UserInterface/PythonRenderTargetModule.cpp). Design: docs/FEATURE-MODEL-PREVIEW.md.
|
||||
static const char c_szModelPreviewScript[] = R"PY(# -*- coding: utf-8 -*-
|
||||
import app
|
||||
import chat
|
||||
import game
|
||||
import grp
|
||||
import item
|
||||
import player
|
||||
import renderTarget
|
||||
import ui
|
||||
import wndMgr
|
||||
from mt_biolog import Action, Text, L
|
||||
|
||||
INDEX=1
|
||||
VIEW_W=300
|
||||
VIEW_H=330
|
||||
MARGIN=15
|
||||
TOP=58
|
||||
ROW=54
|
||||
ZOOM_MIN=-8
|
||||
ZOOM_MAX=12
|
||||
ITEM_ZOOM=2
|
||||
PITCH_LIMIT=60.
|
||||
# Paired by job: warrior, assassin, sura, shaman; male first. race%4 is the job, these races are male.
|
||||
RACES=(0,4,5,1,2,6,7,3)
|
||||
MALE=(0,5,2,7)
|
||||
JOBS=('武士','刺客','修罗','法师')
|
||||
MOTIONS=((1,'待机'),(13,'攻击'),(5,'受击'),(11,'死亡'),(33,'技能1'),(34,'技能2'),(35,'技能3'),(36,'技能4'))
|
||||
|
||||
g_window=None
|
||||
|
||||
|
||||
def RaceName(race):
|
||||
return '%s(%s)'%(JOBS[race%4],'男' if race in MALE else '女')
|
||||
|
||||
|
||||
def ItemSpec(vnum):
|
||||
# (slot, races that may wear it) for a wearable item with a model, None otherwise.
|
||||
try:
|
||||
item.SelectItem(vnum)
|
||||
kind=item.GetItemType();sub=item.GetItemSubType()
|
||||
except (RuntimeError,TypeError):return None
|
||||
if kind==item.ITEM_TYPE_WEAPON and sub!=item.WEAPON_ARROW:slot='weapon'
|
||||
elif kind==item.ITEM_TYPE_ARMOR and sub==item.ARMOR_BODY:slot='armor'
|
||||
elif kind==item.ITEM_TYPE_COSTUME and sub==item.COSTUME_TYPE_BODY:slot='armor'
|
||||
elif kind==item.ITEM_TYPE_COSTUME and sub==item.COSTUME_TYPE_HAIR:slot='hair'
|
||||
else:return None
|
||||
jobs=(item.ITEM_ANTIFLAG_WARRIOR,item.ITEM_ANTIFLAG_ASSASSIN,item.ITEM_ANTIFLAG_SURA,item.ITEM_ANTIFLAG_SHAMAN)
|
||||
races=[r for r in RACES if not item.IsAntiFlag(jobs[r%4]) and
|
||||
not item.IsAntiFlag(item.ITEM_ANTIFLAG_MALE if r in MALE else item.ITEM_ANTIFLAG_FEMALE)]
|
||||
if not races:return None
|
||||
return slot,races
|
||||
|
||||
|
||||
def Previewable(kind,vnum):
|
||||
return kind=='mob' or ItemSpec(vnum) is not None
|
||||
|
||||
|
||||
class View(ui.Window):
|
||||
# The render target is drawn over this window; a left drag turns the camera around the model.
|
||||
def __init__(self,owner):
|
||||
ui.Window.__init__(self);self.owner=owner;self.drag=None
|
||||
|
||||
def OnMouseLeftButtonDown(self):
|
||||
x,y=wndMgr.GetMousePosition();o=self.owner;self.drag=(x,y,o.yaw,o.pitch);return True
|
||||
|
||||
def OnMouseLeftButtonUp(self):
|
||||
self.drag=None;return True
|
||||
|
||||
def OnUpdate(self):
|
||||
if self.drag:
|
||||
x,y=wndMgr.GetMousePosition();x0,y0,yaw,pitch=self.drag
|
||||
self.owner.Turn(yaw+(x-x0)*.5,pitch+(y-y0)*.3)
|
||||
|
||||
def OnRender(self):
|
||||
x,y=self.GetGlobalPosition();w=self.GetWidth();h=self.GetHeight()
|
||||
grp.SetColor(0xff000000);grp.RenderBar(x,y,w,h)
|
||||
renderTarget.RenderTexture(INDEX,x,y,x+w,y+h)
|
||||
grp.SetColor(0xff9b7b48);grp.RenderBox(x,y,w,h)
|
||||
|
||||
|
||||
class PreviewWindow(ui.Window):
|
||||
def __init__(self):
|
||||
ui.Window.__init__(self);self.SetWindowName('ModelPreviewWindow');self.AddFlag('float');self.AddFlag('movable')
|
||||
width=VIEW_W+2*MARGIN;self.SetSize(width,TOP+VIEW_H+ROW+15)
|
||||
self.heading=Text(self,18,18,'模型预览',0xffffd68b)
|
||||
self.closeButton=Action(self,width-76,4,64,'关闭',self.Close)
|
||||
self.view=View(self);self.view.SetParent(self);self.view.SetPosition(MARGIN,TOP);self.view.SetSize(VIEW_W,VIEW_H);self.view.Show()
|
||||
self.note=Text(self,MARGIN+8,TOP+8,'',0xffc3b7a1)
|
||||
y=TOP+VIEW_H+6
|
||||
self.prevRace=Action(self,MARGIN,y,56,'<',lambda:self.StepRace(-1))
|
||||
self.raceName=Text(self,width/2,y+16,'',0xffe8dec8);self.raceName.SetHorizontalAlignCenter()
|
||||
self.nextRace=Action(self,width-MARGIN-56,y,56,'>',lambda:self.StepRace(1))
|
||||
step=(VIEW_W+6)/4
|
||||
self.motions=[Action(self,MARGIN+(i%4)*step,y+(i//4)*ROW,step-6,name,(lambda m=motion:self.Motion(m)))
|
||||
for i,(motion,name) in enumerate(MOTIONS)]
|
||||
self.controls=[Action(self,MARGIN+i*step,0,step-6,name,event) for i,(name,event) in enumerate((
|
||||
('放大',lambda:self.Zoom(-1)),('缩小',lambda:self.Zoom(1)),('自动旋转',self.ToggleSpin),('复位',self.Apply)))]
|
||||
self.kind=None;self.vnum=0;self.slot=None;self.races=[];self.race=0
|
||||
self.yaw=0.;self.pitch=0.;self.zoom=0;self.spin=False;self.ready=False
|
||||
self.Hide()
|
||||
|
||||
def Open(self,kind,vnum):
|
||||
if kind=='mob':slot,races=None,[]
|
||||
else:
|
||||
spec=ItemSpec(vnum)
|
||||
if not spec:return False
|
||||
slot,races=spec
|
||||
renderTarget.Remove(INDEX)
|
||||
if not renderTarget.Create(INDEX,VIEW_W,VIEW_H):return False
|
||||
renderTarget.SetVisibility(INDEX,True)
|
||||
self.kind=kind;self.vnum=vnum;self.slot=slot;self.races=races
|
||||
if races:
|
||||
try:own=player.GetRace()
|
||||
except RuntimeError:own=-1
|
||||
self.race=own if own in races else races[0]
|
||||
self.Layout();self.Apply()
|
||||
if not self.IsShow():self.SetCenterPosition()
|
||||
ui.Window.Show(self);self.SetTop()
|
||||
return True
|
||||
|
||||
def Layout(self):
|
||||
item_mode=self.kind!='mob';rows=1 if item_mode else (len(MOTIONS)+3)//4
|
||||
y=TOP+VIEW_H+6
|
||||
for w in (self.prevRace,self.nextRace,self.raceName):
|
||||
if item_mode:w.Show()
|
||||
else:w.Hide()
|
||||
for b in self.motions:
|
||||
if item_mode:b.Hide()
|
||||
else:b.Show()
|
||||
bottom=y+rows*ROW
|
||||
step=(VIEW_W+6)/4
|
||||
for i,b in enumerate(self.controls):b.SetPosition(MARGIN+i*step,bottom)
|
||||
self.SetSize(VIEW_W+2*MARGIN,bottom+ROW+10)
|
||||
many=len(self.races)>1
|
||||
self.prevRace.SetEnabled(many);self.nextRace.SetEnabled(many)
|
||||
|
||||
def Apply(self):
|
||||
# Rebuilds the model: also the reset button, since SelectModel puts the camera back.
|
||||
renderTarget.ResetModel(INDEX)
|
||||
self.yaw=0.;self.pitch=0.;self.zoom=0
|
||||
if self.kind=='mob':
|
||||
self.ready=bool(renderTarget.SelectModel(INDEX,self.vnum))
|
||||
note='' if self.ready else '该怪物没有模型'
|
||||
else:
|
||||
self.ready=bool(renderTarget.SelectModel(INDEX,self.race))
|
||||
if self.ready:
|
||||
if self.slot=='armor':renderTarget.SetArmor(INDEX,self.vnum)
|
||||
elif self.slot=='weapon':renderTarget.SetWeapon(INDEX,self.vnum)
|
||||
else:renderTarget.SetHair(INDEX,self.vnum)
|
||||
# The default camera cuts the feet of a player race in this view; start two zoom steps out.
|
||||
if self.ready:
|
||||
for _ in range(ITEM_ZOOM):renderTarget.Zoom(INDEX,1)
|
||||
note='' if self.ready else '模型加载失败'
|
||||
self.raceName.SetText(L(RaceName(self.race)))
|
||||
renderTarget.SetRotation(INDEX,self.spin)
|
||||
renderTarget.RotateEyeAroundTarget(INDEX,0.,0.)
|
||||
self.note.SetText(L(note))
|
||||
|
||||
def StepRace(self,delta):
|
||||
if len(self.races)<2:return
|
||||
self.race=self.races[(self.races.index(self.race)+delta)%len(self.races)];self.Apply()
|
||||
|
||||
def Motion(self,motion):
|
||||
if self.ready and not renderTarget.SetMotionIndex(INDEX,motion):self.note.SetText(L('该怪物没有此动作'))
|
||||
else:self.note.SetText('')
|
||||
|
||||
def Zoom(self,direction):
|
||||
if not self.ready or not ZOOM_MIN<=self.zoom+direction<=ZOOM_MAX:return
|
||||
self.zoom+=direction;renderTarget.Zoom(INDEX,direction)
|
||||
|
||||
def Turn(self,yaw,pitch):
|
||||
self.yaw=yaw%360.;self.pitch=max(-PITCH_LIMIT,min(PITCH_LIMIT,pitch))
|
||||
renderTarget.RotateEyeAroundTarget(INDEX,self.pitch,self.yaw)
|
||||
|
||||
def ToggleSpin(self):
|
||||
self.spin=not self.spin;renderTarget.SetRotation(INDEX,self.spin)
|
||||
self.controls[2].SetText('停止旋转' if self.spin else '自动旋转')
|
||||
|
||||
def Close(self):
|
||||
self.Hide()
|
||||
|
||||
def Hide(self):
|
||||
# The target holds a GPU texture and a model; drop both whenever the window is not shown.
|
||||
# ui.Window.__init__ hides the window before the view exists.
|
||||
if getattr(self,'view',None):self.view.drag=None
|
||||
self.ready=False
|
||||
renderTarget.Remove(INDEX)
|
||||
ui.Window.Hide(self)
|
||||
|
||||
def OnRender(self):
|
||||
x,y=self.GetGlobalPosition();grp.SetColor(0xf51b1916);grp.RenderBar(x,y,self.GetWidth(),self.GetHeight());grp.SetColor(0xff9b7b48);grp.RenderBox(x,y,self.GetWidth(),self.GetHeight())
|
||||
|
||||
def OnPressEscapeKey(self):self.Hide();return True
|
||||
|
||||
def Destroy(self):
|
||||
self.Hide();self.closeButton.event=None;self.prevRace.event=self.nextRace.event=None
|
||||
for b in self.motions+self.controls:b.event=None
|
||||
self.view.owner=None
|
||||
|
||||
|
||||
def Preview(kind,vnum):
|
||||
if not g_window:return False
|
||||
if not g_window.Open(kind,vnum):
|
||||
chat.AppendChat(chat.CHAT_TYPE_INFO,L('该物品没有可预览的模型'));return False
|
||||
return True
|
||||
|
||||
|
||||
def KeyHeld():
|
||||
return app.IsPressed(app.DIK_X)
|
||||
|
||||
|
||||
_open=game.GameWindow.Open
|
||||
_close=game.GameWindow.Close
|
||||
|
||||
def Open(self):
|
||||
global g_window
|
||||
_open(self);g_window=PreviewWindow()
|
||||
|
||||
def Close(self):
|
||||
global g_window
|
||||
if g_window:g_window.Destroy();g_window=None
|
||||
renderTarget.Destroy()
|
||||
_close(self)
|
||||
|
||||
game.GameWindow.Open=Open
|
||||
game.GameWindow.Close=Close
|
||||
|
||||
try:
|
||||
import uiInventory
|
||||
_inventorySelect=uiInventory.InventoryWindow.SelectItemSlot
|
||||
def InventorySelect(self,slot):
|
||||
if KeyHeld():
|
||||
vnum=player.GetItemIndex(self._InventoryWindow__InventoryLocalSlotPosToGlobalSlotPos(slot))
|
||||
if vnum and ItemSpec(vnum):Preview('item',vnum);return
|
||||
return _inventorySelect(self,slot)
|
||||
uiInventory.InventoryWindow.SelectItemSlot=InventorySelect
|
||||
except (ImportError,AttributeError):pass
|
||||
|
||||
try:
|
||||
import shop
|
||||
import uiShop
|
||||
_shopSelect=uiShop.ShopDialog.SelectItemSlot
|
||||
def ShopSelect(self,slot):
|
||||
if KeyHeld():
|
||||
real=getattr(self,'_ShopDialog__GetRealIndex',None)
|
||||
vnum=shop.GetItemID(real(slot) if real else slot)
|
||||
if vnum and ItemSpec(vnum):Preview('item',vnum);return
|
||||
return _shopSelect(self,slot)
|
||||
uiShop.ShopDialog.SelectItemSlot=ShopSelect
|
||||
except (ImportError,AttributeError):pass
|
||||
)PY";
|
||||
@@ -44,6 +44,7 @@
|
||||
#include "UserInterface/PythonSystem.h"
|
||||
#include "UserInterface/AbstractApplication.h"
|
||||
#include "UserInterface/PythonApplication.h"
|
||||
#include "UserInterface/CRenderTargetManager.h"
|
||||
#include "EterBase/Timer.h"
|
||||
#include "PythonBoot.h"
|
||||
#include "../UserInterface/UserInterface.h"
|
||||
@@ -157,6 +158,7 @@ struct GameSingletons
|
||||
CGuildMarkDownloader kGuildMarkDownloader;
|
||||
CGuildMarkUploader kGuildMarkUploader;
|
||||
CPythonSystem pySystem; // last CPythonApplication member (m_pySystem)
|
||||
CRenderTargetManager renderTargets; // PORT: AVA2 model preview; destroyed first, its models use the rest
|
||||
};
|
||||
std::unique_ptr<GameSingletons> g_game_singletons;
|
||||
bool g_host_hardware_cursor_enabled = false;
|
||||
@@ -911,6 +913,7 @@ bool run_main_script_body(const char* lpCmdLine, std::string* error)
|
||||
initsafebox();
|
||||
initguild();
|
||||
initServerStateChecker();
|
||||
initRenderTarget(); // PORT: AVA2 model preview
|
||||
init_2v0_gameplay_stubs();
|
||||
|
||||
ClearLogBoxMessage();
|
||||
@@ -1006,6 +1009,7 @@ namespace PythonBoot
|
||||
#include "ChannelScript.inc"
|
||||
#include "BossScript.inc"
|
||||
#include "VipScript.inc"
|
||||
#include "ModelPreviewScript.inc"
|
||||
|
||||
static void install_script_module(const char* c_szModule, const char* c_szScript)
|
||||
{
|
||||
@@ -1086,6 +1090,7 @@ bool RunMainScript(const char* lpCmdLine, std::string* error)
|
||||
install_script_module("mt_channel", c_szChannelScript);
|
||||
install_script_module("mt_boss", c_szBossScript);
|
||||
install_script_module("mt_vip", c_szVipScript);
|
||||
install_script_module("mt_preview", c_szModelPreviewScript);
|
||||
if (g_touch_input)
|
||||
install_script_module("mt_mobile_hud", c_szMobileHUDScript);
|
||||
}
|
||||
|
||||
@@ -184,6 +184,9 @@ class WikiWindow(ui.Window):
|
||||
if row:self.Select('mobs',row)
|
||||
else:self.QueryRemote('mob',vnum[1],0)
|
||||
elif vnum[0]=='items':self.Open(vnum[1])
|
||||
elif vnum[0]=='preview':
|
||||
import mt_preview
|
||||
mt_preview.Preview(vnum[1],vnum[2])
|
||||
return
|
||||
row=self.catalogue.FindItem(vnum)
|
||||
if row:self.Select('items',row)
|
||||
@@ -232,12 +235,15 @@ class WikiWindow(ui.Window):
|
||||
else:
|
||||
cost=self.catalogue.FindItem(offer['cost'])
|
||||
lines.append((u'研究兑换:'+(cost['name'] if cost else u'#%d'%offer['cost'])+u' ×%d'%offer['amount'],offer['cost'] if cost else 0))
|
||||
if row['kind'] in ('ITEM_WEAPON','ITEM_ARMOR','ITEM_COSTUME'):
|
||||
import mt_preview
|
||||
if mt_preview.Previewable('item',vnum):lines.append((u'模型预览',('preview','item',vnum)))
|
||||
lines.append((u'查看获取来源',('query','sources',vnum)))
|
||||
if row['kind'] in ('ITEM_GIFTBOX','ITEM_TREASURE_BOX'):lines.append((u'宝箱奖励预览',('query','chest',vnum)))
|
||||
else:
|
||||
lines=[(u'类型:'+U('矿石' if row['kind']=='STONE' else '怪物')+u' / '+U(RANK_NAMES.get(row['rank'],'普通')),0),(u'等级:%d 生命:%d'%(row['level'],row['hp']),0),
|
||||
(u'基础攻击:%d—%d'%(row['attack_min'],row['attack_max']),0),
|
||||
(u'基础防御:%d 经验:%d'%(row['defense'],row['exp']),0),(u'查看服务端掉落列表',('query','mob',vnum))]
|
||||
(u'基础防御:%d 经验:%d'%(row['defense'],row['exp']),0),(u'模型预览',('preview','mob',vnum)),(u'查看服务端掉落列表',('query','mob',vnum))]
|
||||
if self.remote:lines=self.RemoteLines()
|
||||
self.links=lines
|
||||
pages=max(1,(len(lines)+self.rowCount-1)//self.rowCount)
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "UserInterface/PythonSystem.h"
|
||||
#include "../ScriptLib/PythonBoot.h"
|
||||
#include "ServerClock.h"
|
||||
#include "UserInterface/CRenderTargetManager.h"
|
||||
#include "../EterBase/PerfCounters.h"
|
||||
|
||||
#if defined(__linux__) || defined(__ANDROID__)
|
||||
@@ -213,6 +214,7 @@ void CPythonApplication::RenderGame()
|
||||
CCullingManager::Instance().Process();
|
||||
|
||||
CPythonCharacterManager::Instance().Deform();
|
||||
CRenderTargetManager::Instance().DeformModels(); // PORT: AVA2 model preview
|
||||
CEffectManager::Instance().Update();
|
||||
|
||||
if (!SkipDiagnosticRender("characters")) CPythonBackground::Instance().RenderCharacterShadowToTexture();
|
||||
@@ -239,6 +241,7 @@ void CPythonApplication::RenderGame()
|
||||
|
||||
CPythonBackground::Instance().SetCharacterDirLight();
|
||||
if (!SkipDiagnosticRender("characters")) CPythonCharacterManager::Instance().Render();
|
||||
CRenderTargetManager::Instance().RenderModels(); // PORT: AVA2, under the character light
|
||||
|
||||
CPythonBackground::Instance().SetBackgroundDirLight();
|
||||
CPythonBackground::Instance().RenderWater();
|
||||
@@ -279,6 +282,8 @@ void CPythonApplication::UpdateGame()
|
||||
s.BuildViewFrustum();
|
||||
}
|
||||
|
||||
CRenderTargetManager::Instance().UpdateModels(); // PORT: AVA2 model preview
|
||||
|
||||
TPixelPosition kPPosMainActor;
|
||||
CPythonPlayer::Instance().NEW_GetMainActorPosition(&kPPosMainActor);
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@ class CGraphicBase
|
||||
void SetOrtho3D(float hres, float vres, float zmin, float zmax);
|
||||
void SetPerspective(float fov, float aspect, float nearz, float farz);
|
||||
float GetFOV();
|
||||
float GetAspect() { return ms_fAspect; } // PORT: AVA2 model preview restores the perspective
|
||||
void GetClipPlane(float * fNearY, float * fFarY)
|
||||
{
|
||||
*fNearY = ms_fNearY;
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
#include "StdAfx.h"
|
||||
#include "../EterBase/Stl.h"
|
||||
#include "GrpRenderTargetTexture.h"
|
||||
#include "StateManager.h"
|
||||
|
||||
CGraphicRenderTargetTexture::CGraphicRenderTargetTexture(): m_d3dFormat{ D3DFMT_UNKNOWN }, m_depthStencilFormat{ D3DFMT_UNKNOWN }
|
||||
{
|
||||
Initialize();
|
||||
memset(&m_renderRect, 0, sizeof(m_renderRect));
|
||||
}
|
||||
|
||||
CGraphicRenderTargetTexture::~CGraphicRenderTargetTexture()
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
|
||||
void CGraphicRenderTargetTexture::ReleaseTextures()
|
||||
{
|
||||
safe_release(m_lpd3dRenderTexture);
|
||||
safe_release(m_lpd3dRenderTargetSurface);
|
||||
safe_release(m_lpd3dDepthSurface);
|
||||
safe_release(m_lpd3dOriginalRenderTarget);
|
||||
safe_release(m_lpd3dOldDepthBufferSurface);
|
||||
memset(&m_renderRect, 0, sizeof(m_renderRect));
|
||||
}
|
||||
|
||||
bool CGraphicRenderTargetTexture::Create(const int width, const int height, const D3DFORMAT texFormat, const D3DFORMAT depthFormat)
|
||||
{
|
||||
Reset();
|
||||
m_height = height;
|
||||
m_width = width;
|
||||
if (!CreateRenderTexture(width, height, texFormat))
|
||||
return false;
|
||||
if (!CreateRenderDepthStencil(width, height, depthFormat))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void CGraphicRenderTargetTexture::CreateTextures()
|
||||
{
|
||||
if (CreateRenderTexture(m_width, m_height, m_d3dFormat))
|
||||
CreateRenderDepthStencil(m_width, m_height, m_depthStencilFormat);
|
||||
}
|
||||
|
||||
bool CGraphicRenderTargetTexture::CreateRenderTexture(const int width, const int height, const D3DFORMAT format)
|
||||
{
|
||||
m_d3dFormat = format;
|
||||
// PORT: one level (AVA2 asked for a full chain, 0). Only level 0 is a render target on the recording
|
||||
// device, and the preview is drawn 1:1.
|
||||
if (FAILED(ms_lpd3dDevice->CreateTexture(width, height, 1, D3DUSAGE_RENDERTARGET, m_d3dFormat, D3DPOOL_DEFAULT, &m_lpd3dRenderTexture)))
|
||||
return false;
|
||||
if (FAILED(m_lpd3dRenderTexture->GetSurfaceLevel(0, &m_lpd3dRenderTargetSurface)))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CGraphicRenderTargetTexture::CreateRenderDepthStencil(const int width, const int height, const D3DFORMAT format)
|
||||
{
|
||||
m_depthStencilFormat = format;
|
||||
|
||||
return (ms_lpd3dDevice->CreateDepthStencilSurface(width, height, m_depthStencilFormat, D3DMULTISAMPLE_NONE, &m_lpd3dDepthSurface)) == D3D_OK;
|
||||
}
|
||||
|
||||
void CGraphicRenderTargetTexture::SetRenderTarget()
|
||||
{
|
||||
ms_lpd3dDevice->GetRenderTarget(&m_lpd3dOriginalRenderTarget);
|
||||
ms_lpd3dDevice->GetDepthStencilSurface(&m_lpd3dOldDepthBufferSurface);
|
||||
ms_lpd3dDevice->SetRenderTarget(m_lpd3dRenderTargetSurface, m_lpd3dDepthSurface);
|
||||
}
|
||||
|
||||
void CGraphicRenderTargetTexture::ResetRenderTarget()
|
||||
{
|
||||
ms_lpd3dDevice->SetRenderTarget(m_lpd3dOriginalRenderTarget, m_lpd3dOldDepthBufferSurface);
|
||||
|
||||
safe_release(m_lpd3dOriginalRenderTarget);
|
||||
safe_release(m_lpd3dOldDepthBufferSurface);
|
||||
}
|
||||
|
||||
void CGraphicRenderTargetTexture::Clear()
|
||||
{
|
||||
ms_lpd3dDevice->Clear(0, nullptr, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0x00000000 /* PORT: D3DCOLOR_ARGB(0, 0, 0, 0) */, 1.0f, 0);
|
||||
}
|
||||
|
||||
void CGraphicRenderTargetTexture::Render() const
|
||||
{
|
||||
D3DSURFACE_DESC desc;
|
||||
m_lpd3dRenderTargetSurface->GetDesc(&desc);
|
||||
|
||||
const auto imgWidth = static_cast<float>(m_renderRect.right - m_renderRect.left);
|
||||
const auto imgHeight = static_cast<float>(m_renderRect.bottom - m_renderRect.top);
|
||||
|
||||
const auto su = 0.0f;
|
||||
const auto sv = 0.0f;
|
||||
const auto eu = imgWidth / static_cast<float>(desc.Width);
|
||||
const auto ev = imgHeight / static_cast<float>(desc.Height);
|
||||
|
||||
TPDTVertex pVertices[4];
|
||||
|
||||
pVertices[0].position = TPosition(m_renderRect.left - 0.5f, m_renderRect.top - 0.5f, 0.0f);
|
||||
pVertices[0].texCoord = TTextureCoordinate(su, sv);
|
||||
pVertices[0].diffuse = 0xffffffff;
|
||||
|
||||
pVertices[1].position = TPosition(m_renderRect.right - 0.5f, m_renderRect.top - 0.5f, 0.0f);
|
||||
pVertices[1].texCoord = TTextureCoordinate(eu, sv);
|
||||
pVertices[1].diffuse = 0xffffffff;
|
||||
|
||||
pVertices[2].position = TPosition(m_renderRect.left - 0.5f, m_renderRect.bottom - 0.5f, 0.0f);
|
||||
pVertices[2].texCoord = TTextureCoordinate(su, ev);
|
||||
pVertices[2].diffuse = 0xffffffff;
|
||||
|
||||
pVertices[3].position = TPosition(m_renderRect.right - 0.5f, m_renderRect.bottom - 0.5f, 0.0f);
|
||||
pVertices[3].texCoord = TTextureCoordinate(eu, ev);
|
||||
pVertices[3].diffuse = 0xffffffff;
|
||||
|
||||
if (SetPDTStream(pVertices, 4))
|
||||
{
|
||||
SetDefaultIndexBuffer(DEFAULT_IB_FILL_RECT);
|
||||
STATEMANAGER.SetTexture(0, GetRenderTargetTexture());
|
||||
STATEMANAGER.SetTexture(1, nullptr);
|
||||
STATEMANAGER.SetVertexShader(D3DFVF_XYZ | D3DFVF_TEX1 | D3DFVF_DIFFUSE);
|
||||
STATEMANAGER.DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 4, 0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
void CGraphicRenderTargetTexture::Reset()
|
||||
{
|
||||
Destroy();
|
||||
ReleaseTextures();
|
||||
m_d3dFormat = D3DFMT_UNKNOWN;
|
||||
m_depthStencilFormat = D3DFMT_UNKNOWN;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
// PORT: AVA2 CGraphicRenderTargetTexture (model preview). On the recording device a 32-bit render-target
|
||||
// texture is a GPU colour target ("rt:<id>:<w>x<h>:c"); see platform/EterLib/RecordingDevice.cpp.
|
||||
#include "GrpBase.h"
|
||||
#include "GrpTexture.h"
|
||||
|
||||
class CGraphicRenderTargetTexture : public CGraphicTexture
|
||||
{
|
||||
public:
|
||||
CGraphicRenderTargetTexture();
|
||||
virtual ~CGraphicRenderTargetTexture();
|
||||
|
||||
public:
|
||||
bool Create(int width, int height, D3DFORMAT texFormat, D3DFORMAT depthFormat);
|
||||
void CreateTextures();
|
||||
bool CreateRenderTexture(int width, int height, D3DFORMAT format);
|
||||
bool CreateRenderDepthStencil(int width, int height, D3DFORMAT format);
|
||||
|
||||
void SetRenderTarget();
|
||||
void ResetRenderTarget();
|
||||
|
||||
void SetRenderingRect(RECT* rect){ m_renderRect = *rect; }
|
||||
RECT* GetRenderingRect(){ return &m_renderRect; }
|
||||
|
||||
LPDIRECT3DTEXTURE8 GetRenderTargetTexture() const { return m_lpd3dRenderTexture; }
|
||||
|
||||
void ReleaseTextures();
|
||||
static void Clear();
|
||||
|
||||
void Render() const;
|
||||
|
||||
protected:
|
||||
void Reset();
|
||||
LPDIRECT3DTEXTURE8 m_lpd3dRenderTexture{};
|
||||
LPDIRECT3DSURFACE8 m_lpd3dRenderTargetSurface{};
|
||||
LPDIRECT3DSURFACE8 m_lpd3dDepthSurface{};
|
||||
|
||||
LPDIRECT3DSURFACE8 m_lpd3dOriginalRenderTarget{};
|
||||
LPDIRECT3DSURFACE8 m_lpd3dOldDepthBufferSurface{};
|
||||
D3DFORMAT m_d3dFormat;
|
||||
D3DFORMAT m_depthStencilFormat;
|
||||
RECT m_renderRect{};
|
||||
};
|
||||
@@ -0,0 +1,324 @@
|
||||
#include "StdAfx.h"
|
||||
#include "CRenderTarget.h"
|
||||
#include "InstanceBase.h"
|
||||
|
||||
#include "../EterLib/Camera.h"
|
||||
#include "../EterLib/GrpImageInstance.h"
|
||||
#include "../EterLib/ResourceManager.h"
|
||||
#include "../EterLib/StateManager.h"
|
||||
#include "../EterPythonLib/PythonGraphic.h"
|
||||
|
||||
CRenderTarget::~CRenderTarget() {}
|
||||
|
||||
CRenderTarget::CRenderTarget(const DWORD width, const DWORD height) :
|
||||
m_pModel(nullptr),
|
||||
m_background(nullptr),
|
||||
m_modelRotation(0),
|
||||
m_pmodelRotationActive(true),
|
||||
m_visible(false),
|
||||
m_bShowModel(true),
|
||||
m_bScale(1.0),
|
||||
m_v3AngularVelocity(0.0f, 0.0f, 0.0f),
|
||||
m_v3Eye(0.0f, 0.0f, 0.0f),
|
||||
m_v3Target(0.0f, 0.0f, 0.0f),
|
||||
m_v3Up(0.0f, 0.0f, 1.0f)
|
||||
{
|
||||
auto pTex = std::make_unique<CGraphicRenderTargetTexture>();
|
||||
if (!pTex->Create(width, height, D3DFMT_X8R8G8B8, D3DFMT_D16))
|
||||
{
|
||||
TraceError("CRenderTarget::CRenderTarget: Could not create CGraphicRenderTargetTexture %dx%d", width, height);
|
||||
throw std::runtime_error("CRenderTarget::CRenderTarget: Could not create CGraphicRenderTargetTexture");
|
||||
}
|
||||
|
||||
m_renderTargetTexture = std::move(pTex);
|
||||
}
|
||||
|
||||
void CRenderTarget::RenderTexture() const
|
||||
{
|
||||
m_renderTargetTexture->Render();
|
||||
}
|
||||
|
||||
void CRenderTarget::SetRenderingRect(RECT* rect) const
|
||||
{
|
||||
m_renderTargetTexture->SetRenderingRect(rect);
|
||||
}
|
||||
|
||||
void CRenderTarget::CreateTextures() const
|
||||
{
|
||||
m_renderTargetTexture->CreateTextures();
|
||||
}
|
||||
|
||||
void CRenderTarget::ReleaseTextures() const
|
||||
{
|
||||
m_renderTargetTexture->ReleaseTextures();
|
||||
}
|
||||
|
||||
void CRenderTarget::ResetModel()
|
||||
{
|
||||
// PORT: AVA2 also cleared its wiki affects here; the port has none.
|
||||
m_pModel.reset();
|
||||
}
|
||||
|
||||
void CRenderTarget::SetScale(float fScale)
|
||||
{
|
||||
m_bScale = fScale;
|
||||
if (IsReady())
|
||||
SelectModel(m_pModel->GetRace(), true);
|
||||
}
|
||||
|
||||
void CRenderTarget::SelectModel(const int index, bool isFromScale)
|
||||
{
|
||||
if (m_pModel && !isFromScale && m_pModel->GetRace() == index)
|
||||
return;
|
||||
|
||||
if (isFromScale && m_pModel)
|
||||
m_pModel.reset();
|
||||
|
||||
CInstanceBase::SCreateData kCreateData{};
|
||||
// PORT: AVA2 created every model as TYPE_NPC. Here CActorInstance reserves the armor/weapon/hair parts for
|
||||
// TYPE_PC only, so a player race created as an NPC crashes on the first ChangeArmor.
|
||||
kCreateData.m_bType = index < NRaceData::JOB_MAX_NUM * 2 ? CActorInstance::TYPE_PC : CActorInstance::TYPE_NPC;
|
||||
kCreateData.m_dwRace = index;
|
||||
auto model = std::make_unique<CInstanceBase>();
|
||||
if (!model->Create(kCreateData))
|
||||
{
|
||||
ResetModel();
|
||||
return;
|
||||
}
|
||||
m_pModel = std::move(model);
|
||||
// PORT: the preview has no name; the empty text tail Create attaches would follow the world origin.
|
||||
m_pModel->DetachTextTail();
|
||||
m_pModel->NEW_SetPixelPosition(TPixelPosition(0, 0, 0));
|
||||
m_pModel->SetRotation(0.0f);
|
||||
|
||||
float fTargetHeight = 110.0f;
|
||||
|
||||
m_v3Eye.x = 0.0f;
|
||||
m_v3Eye.y = -1000.0f;
|
||||
m_v3Eye.z = 600.0f;
|
||||
|
||||
m_v3Target.x = 0.0f;
|
||||
m_v3Target.y = 0.0f;
|
||||
m_v3Target.z = 95.0f;
|
||||
|
||||
if (index > (NRaceData::JOB_MAX_NUM * 2))
|
||||
{
|
||||
const auto ptrGraphicInstance = m_pModel->GetGraphicThingInstancePtr();
|
||||
if (ptrGraphicInstance) {
|
||||
const float fRaceHeight = (ptrGraphicInstance->GetHeight()/m_bScale);
|
||||
fTargetHeight = fRaceHeight / 2.0f;
|
||||
m_v3Eye.y = -(fRaceHeight * 8.9f);
|
||||
m_v3Eye.z = 0.0f;
|
||||
m_v3Target.z = fRaceHeight / 2.0f;
|
||||
}
|
||||
}
|
||||
|
||||
auto& camera_manager = CCameraManager::instance();
|
||||
camera_manager.SetCurrentCamera(CCameraManager::DEFAULT_ORTHO_CAMERA);
|
||||
camera_manager.GetCurrentCamera()->SetTargetHeight(fTargetHeight);
|
||||
camera_manager.ResetToPreviousCamera();
|
||||
m_modelRotation = 0.0f;
|
||||
m_v3AngularVelocity = D3DXVECTOR3(0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
bool CRenderTarget::CreateBackground(const char* imgPath, const DWORD width, const DWORD height)
|
||||
{
|
||||
if (m_background)
|
||||
return false;
|
||||
|
||||
m_background = std::make_unique<CGraphicImageInstance>();
|
||||
|
||||
const auto graphic_image = dynamic_cast<CGraphicImage*>(CResourceManager::instance().GetResourcePointer(imgPath));
|
||||
if (!graphic_image)
|
||||
{
|
||||
m_background.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
m_background->SetImagePointer(graphic_image);
|
||||
return true;
|
||||
}
|
||||
|
||||
// PORT: AVA2 drew its background in a pass of its own through CGraphicImageInstance::Render. The port's image
|
||||
// instance goes to the UI command list, not the device, so RenderModel draws the background into the target
|
||||
// itself: one textured quad that fills it under a target-sized ortho.
|
||||
void CRenderTarget::RenderBackground() const
|
||||
{
|
||||
if (!m_background || m_background->IsEmpty())
|
||||
return;
|
||||
CGraphicTexture* texture = m_background->GetTexturePointer();
|
||||
if (!texture)
|
||||
return;
|
||||
|
||||
const auto& rectRender = *m_renderTargetTexture->GetRenderingRect();
|
||||
const auto width = static_cast<float>(rectRender.right - rectRender.left);
|
||||
const auto height = static_cast<float>(rectRender.bottom - rectRender.top);
|
||||
|
||||
auto& python_graphic = CPythonGraphic::Instance();
|
||||
python_graphic.PushState();
|
||||
STATEMANAGER.SaveTransform(D3DTS_WORLD, &CGraphicBase::GetIdentityMatrix());
|
||||
STATEMANAGER.SetTransform(D3DTS_VIEW, &CGraphicBase::GetIdentityMatrix());
|
||||
python_graphic.SetOrtho2D(width, height, 1.0f);
|
||||
|
||||
STATEMANAGER.SaveRenderState(D3DRS_ZENABLE, FALSE);
|
||||
STATEMANAGER.SaveRenderState(D3DRS_ZWRITEENABLE, FALSE);
|
||||
STATEMANAGER.SaveRenderState(D3DRS_LIGHTING, FALSE);
|
||||
STATEMANAGER.SaveRenderState(D3DRS_FOGENABLE, FALSE);
|
||||
STATEMANAGER.SaveRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
|
||||
STATEMANAGER.SaveRenderState(D3DRS_ALPHATESTENABLE, FALSE);
|
||||
STATEMANAGER.SaveRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
|
||||
STATEMANAGER.SaveTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
|
||||
STATEMANAGER.SaveTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
|
||||
STATEMANAGER.SaveTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
|
||||
STATEMANAGER.SaveTextureStageState(0, D3DTSS_MINFILTER, D3DTEXF_LINEAR);
|
||||
STATEMANAGER.SaveTextureStageState(0, D3DTSS_MAGFILTER, D3DTEXF_LINEAR);
|
||||
STATEMANAGER.SaveTextureStageState(0, D3DTSS_MIPFILTER, D3DTEXF_NONE);
|
||||
STATEMANAGER.SaveTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
|
||||
|
||||
TPDTVertex vertices[4];
|
||||
const float x[4] = { 0.0f, width, 0.0f, width };
|
||||
const float y[4] = { 0.0f, 0.0f, height, height };
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
vertices[i].position = TPosition(x[i], y[i], 0.0f);
|
||||
vertices[i].texCoord = TTextureCoordinate(x[i] / width, y[i] / height);
|
||||
vertices[i].diffuse = 0xffffffff;
|
||||
}
|
||||
STATEMANAGER.SetTexture(0, texture->GetD3DTexture());
|
||||
STATEMANAGER.SetTexture(1, nullptr);
|
||||
STATEMANAGER.SetVertexShader(D3DFVF_XYZ | D3DFVF_TEX1 | D3DFVF_DIFFUSE);
|
||||
STATEMANAGER.DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, vertices, sizeof(TPDTVertex));
|
||||
|
||||
STATEMANAGER.RestoreTextureStageState(1, D3DTSS_COLOROP);
|
||||
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_MIPFILTER);
|
||||
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_MAGFILTER);
|
||||
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_MINFILTER);
|
||||
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_ALPHAOP);
|
||||
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_COLOROP);
|
||||
STATEMANAGER.RestoreTextureStageState(0, D3DTSS_COLORARG1);
|
||||
STATEMANAGER.RestoreRenderState(D3DRS_CULLMODE);
|
||||
STATEMANAGER.RestoreRenderState(D3DRS_ALPHATESTENABLE);
|
||||
STATEMANAGER.RestoreRenderState(D3DRS_ALPHABLENDENABLE);
|
||||
STATEMANAGER.RestoreRenderState(D3DRS_FOGENABLE);
|
||||
STATEMANAGER.RestoreRenderState(D3DRS_LIGHTING);
|
||||
STATEMANAGER.RestoreRenderState(D3DRS_ZWRITEENABLE);
|
||||
STATEMANAGER.RestoreRenderState(D3DRS_ZENABLE);
|
||||
STATEMANAGER.RestoreTransform(D3DTS_WORLD);
|
||||
python_graphic.PopState();
|
||||
}
|
||||
|
||||
void CRenderTarget::UpdateModel()
|
||||
{
|
||||
if (!IsReady())
|
||||
return;
|
||||
|
||||
if (m_pmodelRotationActive && m_bShowModel)
|
||||
{
|
||||
if (m_modelRotation < 360.0f)
|
||||
m_modelRotation += 1.0f / 60 * 60.0f;
|
||||
else
|
||||
m_modelRotation = 0.0f / 60 * 60.0f;
|
||||
|
||||
m_pModel->SetRotation(m_modelRotation);
|
||||
}
|
||||
else
|
||||
m_pModel->SetRotation(0.0f);
|
||||
m_pModel->Transform();
|
||||
if(m_pmodelRotationActive)
|
||||
m_pModel->GetGraphicThingInstanceRef().RotationProcess();
|
||||
}
|
||||
|
||||
void CRenderTarget::DeformModel()
|
||||
{
|
||||
if (!IsReady())
|
||||
return;
|
||||
// PORT: stands in for AVA2's SetAlwaysRender. The model sits at the world origin, so the culling pass hides it.
|
||||
m_pModel->GetGraphicThingInstanceRef().Show();
|
||||
m_pModel->Deform();
|
||||
}
|
||||
|
||||
void CRenderTarget::RenderModel()
|
||||
{
|
||||
if (!IsReady())
|
||||
return;
|
||||
|
||||
auto& python_graphic = CPythonGraphic::Instance();
|
||||
auto& camera_manager = CCameraManager::instance();
|
||||
auto& state_manager = CStateManager::Instance();
|
||||
auto& rectRender = *m_renderTargetTexture->GetRenderingRect();
|
||||
|
||||
const auto width = static_cast<float>(rectRender.right - rectRender.left);
|
||||
const auto height = static_cast<float>(rectRender.bottom - rectRender.top);
|
||||
// PORT: nothing to draw before the window has given its rect.
|
||||
if (width <= 0.0f || height <= 0.0f)
|
||||
return;
|
||||
|
||||
m_renderTargetTexture->SetRenderTarget();
|
||||
m_renderTargetTexture->Clear();
|
||||
python_graphic.SetViewport(0.0f, 0.0f, width, height);
|
||||
RenderBackground();
|
||||
|
||||
python_graphic.ClearDepthBuffer();
|
||||
|
||||
const auto fov = python_graphic.GetFOV();
|
||||
const auto aspect = python_graphic.GetAspect();
|
||||
float near_y, far_y;
|
||||
python_graphic.GetClipPlane(&near_y, &far_y);
|
||||
|
||||
state_manager.SetRenderState(D3DRS_FOGENABLE, FALSE);
|
||||
|
||||
python_graphic.PushState();
|
||||
|
||||
camera_manager.SetCurrentCamera(CCameraManager::DEFAULT_ORTHO_CAMERA);
|
||||
camera_manager.GetCurrentCamera()->SetViewParams(m_v3Eye, m_v3Target, m_v3Up);
|
||||
camera_manager.GetCurrentCamera()->RotateEyeAroundTarget(m_v3AngularVelocity.z, m_v3AngularVelocity.x);
|
||||
|
||||
python_graphic.UpdateViewMatrix();
|
||||
python_graphic.SetPerspective(10.0f, width / height, 100.0f, 15000.0f);
|
||||
|
||||
if (m_bShowModel)
|
||||
{
|
||||
m_pModel->GetGraphicThingInstanceRef().Show();
|
||||
m_pModel->Update();
|
||||
m_pModel->Render();
|
||||
}
|
||||
// PORT: AVA2's RenderAllAttachingEffectWiki (attached effects) is not ported.
|
||||
|
||||
camera_manager.ResetToPreviousCamera();
|
||||
python_graphic.RestoreViewport();
|
||||
python_graphic.PopState();
|
||||
python_graphic.SetPerspective(fov, aspect, near_y, far_y);
|
||||
|
||||
m_renderTargetTexture->ResetRenderTarget();
|
||||
|
||||
state_manager.SetRenderState(D3DRS_FOGENABLE, TRUE);
|
||||
}
|
||||
|
||||
void CRenderTarget::SetModelV3Eye(float x, float y, float z)
|
||||
{
|
||||
m_v3Eye.x = x;
|
||||
m_v3Eye.y = y;
|
||||
m_v3Eye.z = z;
|
||||
}
|
||||
|
||||
void CRenderTarget::SetModelV3Target(float x, float y, float z)
|
||||
{
|
||||
m_v3Target.x = x;
|
||||
m_v3Target.y = y;
|
||||
m_v3Target.z = z;
|
||||
}
|
||||
|
||||
void CRenderTarget::RotateEyeAroundTarget(float fPitchDegree, float fRollDegree)
|
||||
{
|
||||
m_v3AngularVelocity.x = fRollDegree;
|
||||
m_v3AngularVelocity.z = fPitchDegree;
|
||||
}
|
||||
|
||||
void CRenderTarget::Zoom(float fRatio)
|
||||
{
|
||||
if (fRatio == 1.0f)
|
||||
return;
|
||||
D3DXVECTOR3 v3Temp = m_v3Eye - m_v3Target;
|
||||
v3Temp *= fRatio;
|
||||
m_v3Eye = v3Temp + m_v3Target;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
// PORT: AVA2 CRenderTarget (EterLib/CRenderTarget.h): one model drawn into its own colour render target, which
|
||||
// a UI window then shows. It lives here, not in EterLib, because it owns a CInstanceBase.
|
||||
#include <memory>
|
||||
#include "../EterLib/GrpRenderTargetTexture.h"
|
||||
|
||||
class CInstanceBase;
|
||||
class CGraphicImageInstance;
|
||||
|
||||
class CRenderTarget
|
||||
{
|
||||
public:
|
||||
CRenderTarget(DWORD width, DWORD height);
|
||||
~CRenderTarget();
|
||||
|
||||
void SetVisibility(bool isShow){ m_visible = isShow; }
|
||||
void SetRotation(bool bRot) { m_pmodelRotationActive = bRot; }
|
||||
bool IsReady() { return (m_visible && m_pModel); }
|
||||
std::unique_ptr<CInstanceBase>& GetModel() { return m_pModel; }
|
||||
|
||||
void RotateEyeAroundTarget(float fPitchDegree, float fRollDegree);
|
||||
void Zoom(float fRatio);
|
||||
|
||||
void SelectModel(int index, bool isFromScale= false);
|
||||
void ResetModel();
|
||||
|
||||
void UpdateModel();
|
||||
void DeformModel();
|
||||
void RenderModel();
|
||||
void SetRenderingRect(RECT* rect) const;
|
||||
|
||||
void CreateTextures() const;
|
||||
void ReleaseTextures() const;
|
||||
void RenderTexture() const;
|
||||
|
||||
bool CreateBackground(const char* imgPath, DWORD width, DWORD height);
|
||||
void RenderBackground() const;
|
||||
|
||||
void SetModelV3Eye(float x, float y, float z);
|
||||
void SetModelV3Target(float x, float y, float z);
|
||||
|
||||
void SetModelStatus(bool bStatus) { m_bShowModel = bStatus; }
|
||||
|
||||
float GetScale() { return m_bScale; }
|
||||
void SetScale(float fScale);
|
||||
|
||||
bool IsShow() { return m_visible; }
|
||||
|
||||
private:
|
||||
std::unique_ptr<CInstanceBase> m_pModel;
|
||||
std::unique_ptr<CGraphicImageInstance> m_background;
|
||||
std::unique_ptr<CGraphicRenderTargetTexture> m_renderTargetTexture;
|
||||
float m_modelRotation;
|
||||
bool m_pmodelRotationActive;
|
||||
bool m_visible;
|
||||
bool m_bShowModel;
|
||||
float m_bScale;
|
||||
D3DXVECTOR3 m_v3AngularVelocity;
|
||||
D3DXVECTOR3 m_v3Eye;
|
||||
D3DXVECTOR3 m_v3Target;
|
||||
D3DXVECTOR3 m_v3Up;
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
#include "StdAfx.h"
|
||||
#include "CRenderTargetManager.h"
|
||||
#include "../EterBase/Stl.h"
|
||||
#include <memory>
|
||||
|
||||
CRenderTargetManager::CRenderTargetManager(){}
|
||||
CRenderTargetManager::~CRenderTargetManager()
|
||||
{
|
||||
Destroy();
|
||||
}
|
||||
void CRenderTargetManager::Destroy()
|
||||
{
|
||||
m_renderTargets.clear();
|
||||
}
|
||||
void CRenderTargetManager::DeformModels()
|
||||
{
|
||||
for (const auto& elem : m_renderTargets)
|
||||
elem.second->DeformModel();
|
||||
}
|
||||
void CRenderTargetManager::UpdateModels()
|
||||
{
|
||||
for (auto& elem : m_renderTargets)
|
||||
elem.second->UpdateModel();
|
||||
}
|
||||
// PORT: the background is drawn into the target by RenderModel (CRenderTarget::RenderBackground).
|
||||
void CRenderTargetManager::RenderBackgrounds()
|
||||
{
|
||||
}
|
||||
void CRenderTargetManager::RenderModels()
|
||||
{
|
||||
for (const auto& elem : m_renderTargets)
|
||||
elem.second->RenderModel();
|
||||
}
|
||||
void CRenderTargetManager::CreateRenderTargetTextures()
|
||||
{
|
||||
for (const auto& elem : m_renderTargets)
|
||||
elem.second->CreateTextures();
|
||||
}
|
||||
void CRenderTargetManager::ReleaseRenderTargetTextures()
|
||||
{
|
||||
for (const auto& elem : m_renderTargets)
|
||||
elem.second->ReleaseTextures();
|
||||
}
|
||||
bool CRenderTargetManager::CreateRenderTarget(const int index, const int width, const int height)
|
||||
{
|
||||
// PORT: the size check guards the recording device, which allocates the target's bytes up front.
|
||||
if (index < 1 || GetRenderTarget(index) || width < 1 || height < 1 || width > 2048 || height > 2048)
|
||||
return false;
|
||||
try
|
||||
{
|
||||
m_renderTargets.emplace(index, std::make_shared<CRenderTarget>(width, height));
|
||||
}
|
||||
catch (const std::exception&)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void CRenderTargetManager::RemoveRenderTarget(const int index)
|
||||
{
|
||||
const auto it = m_renderTargets.find(index);
|
||||
if (it != m_renderTargets.end())
|
||||
{
|
||||
it->second->ResetModel();
|
||||
m_renderTargets.erase(index);
|
||||
}
|
||||
}
|
||||
std::shared_ptr<CRenderTarget> CRenderTargetManager::GetRenderTarget(const int index)
|
||||
{
|
||||
const auto it = m_renderTargets.find(index);
|
||||
if (it != m_renderTargets.end())
|
||||
return it->second;
|
||||
return nullptr;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
// PORT: AVA2 CRenderTargetManager (EterLib/CRenderTargetManager.h).
|
||||
#include <unordered_map>
|
||||
#include <cstdint>
|
||||
|
||||
#include "../EterBase/Singleton.h"
|
||||
#include <memory>
|
||||
#include "CRenderTarget.h"
|
||||
|
||||
class CRenderTargetManager : public CSingleton<CRenderTargetManager>
|
||||
{
|
||||
public:
|
||||
CRenderTargetManager();
|
||||
virtual ~CRenderTargetManager();
|
||||
|
||||
std::shared_ptr<CRenderTarget> GetRenderTarget(int index);
|
||||
bool CreateRenderTarget(int index, int width, int height);
|
||||
|
||||
void CreateRenderTargetTextures();
|
||||
void ReleaseRenderTargetTextures();
|
||||
|
||||
void RemoveRenderTarget(const int index);
|
||||
void Destroy();
|
||||
void DeformModels();
|
||||
void UpdateModels();
|
||||
void RenderBackgrounds();
|
||||
void RenderModels();
|
||||
|
||||
protected:
|
||||
std::unordered_map<int, std::shared_ptr<CRenderTarget>> m_renderTargets;
|
||||
};
|
||||
@@ -0,0 +1,282 @@
|
||||
// PORT: AVA2 CPythonRenderTargetModule (module renderTarget), the subset the model preview uses. AVA2's window
|
||||
// manager created the target for a registered window and drew it; here Create and RenderTexture do that, called
|
||||
// from the script's own window (ScriptLib/ModelPreviewScript.inc).
|
||||
#include "StdAfx.h"
|
||||
#include "CRenderTargetManager.h"
|
||||
#include "InstanceBase.h"
|
||||
#include "../GameLib/ItemManager.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// The default of CPythonApplication's camera zoom speed (PythonApplicationCamera.cpp), which AVA2 read.
|
||||
const float c_fRenderTargetZoomSpeed = 0.05f;
|
||||
|
||||
std::shared_ptr<CRenderTarget> Target(PyObject* poArgs)
|
||||
{
|
||||
int iIndex;
|
||||
if (!PyTuple_GetInteger(poArgs, 0, &iIndex))
|
||||
return nullptr;
|
||||
return CRenderTargetManager::Instance().GetRenderTarget(iIndex);
|
||||
}
|
||||
|
||||
bool Integer(PyObject* poArgs, int pos, int* value)
|
||||
{
|
||||
return PyTuple_GetInteger(poArgs, pos, value);
|
||||
}
|
||||
}
|
||||
|
||||
PyObject* renderTargetCreate(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
int iIndex, iWidth, iHeight;
|
||||
if (!Integer(poArgs, 0, &iIndex) || !Integer(poArgs, 1, &iWidth) || !Integer(poArgs, 2, &iHeight))
|
||||
return Py_BadArgument();
|
||||
return Py_BuildValue("i", CRenderTargetManager::Instance().CreateRenderTarget(iIndex, iWidth, iHeight) ? 1 : 0);
|
||||
}
|
||||
|
||||
PyObject* renderTargetRemove(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
int iIndex;
|
||||
if (!Integer(poArgs, 0, &iIndex))
|
||||
return Py_BadArgument();
|
||||
CRenderTargetManager::Instance().RemoveRenderTarget(iIndex);
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* renderTargetExists(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
int iIndex;
|
||||
if (!Integer(poArgs, 0, &iIndex))
|
||||
return Py_BadArgument();
|
||||
return Py_BuildValue("i", CRenderTargetManager::Instance().GetRenderTarget(iIndex) ? 1 : 0);
|
||||
}
|
||||
|
||||
PyObject* renderTargetRenderTexture(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
int l, t, r, b;
|
||||
if (!Integer(poArgs, 1, &l) || !Integer(poArgs, 2, &t) || !Integer(poArgs, 3, &r) || !Integer(poArgs, 4, &b))
|
||||
return Py_BadArgument();
|
||||
const auto target = Target(poArgs);
|
||||
if (target && target->IsShow())
|
||||
{
|
||||
RECT rect = { l, t, r, b };
|
||||
target->SetRenderingRect(&rect);
|
||||
target->RenderTexture();
|
||||
}
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* renderTargetSelectModel(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
int modelIndex;
|
||||
if (!Integer(poArgs, 1, &modelIndex))
|
||||
return Py_BadArgument();
|
||||
const auto target = Target(poArgs);
|
||||
if (target)
|
||||
target->SelectModel(modelIndex);
|
||||
return Py_BuildValue("i", target && target->GetModel() ? 1 : 0);
|
||||
}
|
||||
|
||||
PyObject* renderTargetSetArmor(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
int iItemVnum;
|
||||
if (!Integer(poArgs, 1, &iItemVnum))
|
||||
return Py_BadArgument();
|
||||
const auto target = Target(poArgs);
|
||||
if (target && target->IsReady())
|
||||
target->GetModel()->ChangeArmor(iItemVnum);
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* renderTargetSetWeapon(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
int iItemVnum;
|
||||
if (!Integer(poArgs, 1, &iItemVnum))
|
||||
return Py_BadArgument();
|
||||
const auto target = Target(poArgs);
|
||||
if (target && target->IsReady())
|
||||
target->GetModel()->ChangeWeapon(iItemVnum);
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* renderTargetSetHair(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
int iItemVnum;
|
||||
if (!Integer(poArgs, 1, &iItemVnum))
|
||||
return Py_BadArgument();
|
||||
bool isValue = false;
|
||||
const auto target = Target(poArgs);
|
||||
if (target)
|
||||
{
|
||||
PyTuple_GetBoolean(poArgs, 2, &isValue);
|
||||
if (!isValue)
|
||||
{
|
||||
CItemData* pItemData;
|
||||
if (!CItemManager::Instance().GetItemDataPointer(iItemVnum, &pItemData))
|
||||
return Py_BuildNone();
|
||||
iItemVnum = pItemData->GetValue(3);
|
||||
}
|
||||
if (target->IsReady())
|
||||
target->GetModel()->SetHair(iItemVnum);
|
||||
}
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* renderTargetSetMount(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
int iRace;
|
||||
if (!Integer(poArgs, 1, &iRace))
|
||||
return Py_BadArgument();
|
||||
const auto target = Target(poArgs);
|
||||
if (target && target->IsReady())
|
||||
target->GetModel()->MountHorse(iRace);
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* renderTargetSetRotation(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
bool bRot;
|
||||
if (!PyTuple_GetBoolean(poArgs, 1, &bRot))
|
||||
return Py_BadArgument();
|
||||
const auto target = Target(poArgs);
|
||||
if (target)
|
||||
target->SetRotation(bRot);
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* renderTargetSetVisibility(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
bool isShow;
|
||||
if (!PyTuple_GetBoolean(poArgs, 1, &isShow))
|
||||
return Py_BadArgument();
|
||||
const auto target = Target(poArgs);
|
||||
if (target)
|
||||
target->SetVisibility(isShow);
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* renderTargetSetBackground(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
char* szPathName;
|
||||
if (!PyTuple_GetString(poArgs, 1, &szPathName))
|
||||
return Py_BadArgument();
|
||||
const auto target = Target(poArgs);
|
||||
// PORT: the background is stretched over the target when drawn, so the sizes AVA2 passed are unused.
|
||||
return Py_BuildValue("i", target && target->CreateBackground(szPathName, 0, 0) ? 1 : 0);
|
||||
}
|
||||
|
||||
PyObject* renderTargetResetModel(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
const auto target = Target(poArgs);
|
||||
if (target)
|
||||
target->ResetModel();
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* renderTargetDestroy(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
CRenderTargetManager::Instance().Destroy();
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* renderTargetSetModelV3Eye(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
float x, y, z;
|
||||
if (!PyTuple_GetFloat(poArgs, 1, &x) || !PyTuple_GetFloat(poArgs, 2, &y) || !PyTuple_GetFloat(poArgs, 3, &z))
|
||||
return Py_BadArgument();
|
||||
const auto target = Target(poArgs);
|
||||
if (target)
|
||||
target->SetModelV3Eye(x, y, z);
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* renderTargetSetModelV3Target(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
float x, y, z;
|
||||
if (!PyTuple_GetFloat(poArgs, 1, &x) || !PyTuple_GetFloat(poArgs, 2, &y) || !PyTuple_GetFloat(poArgs, 3, &z))
|
||||
return Py_BadArgument();
|
||||
const auto target = Target(poArgs);
|
||||
if (target)
|
||||
target->SetModelV3Target(x, y, z);
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* renderTargetZoom(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
int iDirection;
|
||||
if (!Integer(poArgs, 1, &iDirection))
|
||||
return Py_BadArgument();
|
||||
const auto target = Target(poArgs);
|
||||
if (target && target->IsReady())
|
||||
target->Zoom(1.0f + c_fRenderTargetZoomSpeed * float(iDirection));
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* renderTargetRotateEyeAroundTarget(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
float fPitch, fRoll;
|
||||
if (!PyTuple_GetFloat(poArgs, 1, &fPitch) || !PyTuple_GetFloat(poArgs, 2, &fRoll))
|
||||
return Py_BadArgument();
|
||||
const auto target = Target(poArgs);
|
||||
if (target && target->IsReady())
|
||||
target->RotateEyeAroundTarget(fPitch, fRoll);
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* renderTargetSetScale(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
float fScale;
|
||||
if (!PyTuple_GetFloat(poArgs, 1, &fScale))
|
||||
return Py_BadArgument();
|
||||
const auto target = Target(poArgs);
|
||||
if (target)
|
||||
target->SetScale(fScale);
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject* renderTargetIsShow(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
const auto target = Target(poArgs);
|
||||
return Py_BuildValue("i", target ? target->IsShow() : 0);
|
||||
}
|
||||
|
||||
PyObject* renderTargetSetMotionIndex(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
int iMotionIndex;
|
||||
if (!Integer(poArgs, 1, &iMotionIndex))
|
||||
return Py_BadArgument();
|
||||
const auto target = Target(poArgs);
|
||||
if (target && target->IsReady())
|
||||
return Py_BuildValue("i", target->GetModel()->GetGraphicThingInstanceRef().InterceptOnceMotion(iMotionIndex) ? 1 : 0);
|
||||
return Py_BuildValue("i", 0);
|
||||
}
|
||||
|
||||
void initRenderTarget()
|
||||
{
|
||||
static PyMethodDef s_methods[] =
|
||||
{
|
||||
{ "Create", renderTargetCreate, METH_VARARGS },
|
||||
{ "Remove", renderTargetRemove, METH_VARARGS },
|
||||
{ "Exists", renderTargetExists, METH_VARARGS },
|
||||
{ "RenderTexture", renderTargetRenderTexture, METH_VARARGS },
|
||||
{ "SelectModel", renderTargetSelectModel, METH_VARARGS },
|
||||
{ "SetVisibility", renderTargetSetVisibility, METH_VARARGS },
|
||||
{ "SetBackground", renderTargetSetBackground, METH_VARARGS },
|
||||
{ "SetArmor", renderTargetSetArmor, METH_VARARGS },
|
||||
{ "SetWeapon", renderTargetSetWeapon, METH_VARARGS },
|
||||
{ "SetHair", renderTargetSetHair, METH_VARARGS },
|
||||
{ "SetMount", renderTargetSetMount, METH_VARARGS },
|
||||
{ "SetRotation", renderTargetSetRotation, METH_VARARGS },
|
||||
{ "ResetModel", renderTargetResetModel, METH_VARARGS },
|
||||
{ "Destroy", renderTargetDestroy, METH_VARARGS },
|
||||
{ "SetModelV3Eye", renderTargetSetModelV3Eye, METH_VARARGS },
|
||||
{ "SetModelV3Target", renderTargetSetModelV3Target, METH_VARARGS },
|
||||
{ "Zoom", renderTargetZoom, METH_VARARGS },
|
||||
{ "RotateEyeAroundTarget", renderTargetRotateEyeAroundTarget, METH_VARARGS },
|
||||
{ "SetScale", renderTargetSetScale, METH_VARARGS },
|
||||
{ "IsShow", renderTargetIsShow, METH_VARARGS },
|
||||
{ "SetMotionIndex", renderTargetSetMotionIndex, METH_VARARGS },
|
||||
{ NULL, NULL, 0 },
|
||||
};
|
||||
|
||||
Py_InitModule("renderTarget", s_methods);
|
||||
}
|
||||
@@ -70,6 +70,7 @@ void initMessenger();
|
||||
void initsafebox();
|
||||
void initguild();
|
||||
void initServerStateChecker();
|
||||
void initRenderTarget(); // PORT: AVA2 CPythonRenderTargetModule
|
||||
|
||||
extern const std::string& ApplicationStringTable_GetString(DWORD dwID);
|
||||
extern const std::string& ApplicationStringTable_GetString(DWORD dwID, LPCSTR szKey);
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[2]
|
||||
|
||||
def script(name):return (ROOT/'src/platform/ScriptLib'/name).read_text().split('R"PY(',1)[1].rsplit(')PY";',1)[0]
|
||||
|
||||
|
||||
class Widget(object):
|
||||
def __init__(self,*a,**k):
|
||||
self.Hide() # like ui.Window, before the subclass sets anything
|
||||
self.shown=False;self.text='';self.pos=(0,0);self.size=(0,0);self.enabled=True
|
||||
def __getattr__(self,name):return lambda *a,**k:None
|
||||
def Show(self):self.shown=True
|
||||
def Hide(self):self.shown=False
|
||||
def IsShow(self):return self.shown
|
||||
def SetText(self,text):self.text=text
|
||||
def SetPosition(self,x,y):self.pos=(x,y)
|
||||
def GetGlobalPosition(self):return self.pos
|
||||
def SetSize(self,w,h):self.size=(w,h)
|
||||
def GetWidth(self):return self.size[0]
|
||||
def GetHeight(self):return self.size[1]
|
||||
|
||||
|
||||
class Action(Widget):
|
||||
def __init__(self,parent,x,y,width,text,event):
|
||||
Widget.__init__(self);self.pos=(x,y);self.event=event;self.text=text;self.shown=True
|
||||
def SetEnabled(self,enabled):self.enabled=enabled
|
||||
|
||||
|
||||
# vnum -> (type, subtype, antiflags)
|
||||
ITEMS={11299:(1,0,0),11499:(1,0,0x08|0x10|0x20),12049:(2,0,0x04|0x10|0x20|0x40),8000:(1,6,0),
|
||||
41001:(28,0,0x01),45001:(28,1,0x02),27001:(3,0,0),11259:(2,1,0)}
|
||||
|
||||
|
||||
class Targets(object):
|
||||
"""renderTarget stub: what exists, and every call in order."""
|
||||
def __init__(self):self.live=set();self.calls=[];self.models={};self.missing=set();self.motionless=set()
|
||||
def __getattr__(self,name):
|
||||
def call(*a):self.calls.append((name,)+a)
|
||||
return call
|
||||
def Create(self,i,w,h):self.calls.append(('Create',i,w,h));self.live.add(i);return 1
|
||||
def Remove(self,i):self.calls.append(('Remove',i));self.live.discard(i);self.models.pop(i,None)
|
||||
def Destroy(self):self.calls.append(('Destroy',));self.live.clear();self.models.clear()
|
||||
def SelectModel(self,i,race):
|
||||
self.calls.append(('SelectModel',i,race))
|
||||
if race in self.missing:return 0
|
||||
self.models[i]=race;return 1
|
||||
def SetMotionIndex(self,i,m):self.calls.append(('SetMotionIndex',i,m));return 0 if m in self.motionless else 1
|
||||
def named(self,name):return [c for c in self.calls if c[0]==name]
|
||||
|
||||
|
||||
class PreviewTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.chat=[];self.keys=set();self.mouse=(0,0);self.race=0;self.selected=None
|
||||
self.rt=Targets()
|
||||
names=('app','chat','game','grp','item','player','renderTarget','ui','wndMgr','mt_biolog','uiInventory','uiShop','shop')
|
||||
mods={n:types.ModuleType(n) for n in names}
|
||||
mods['renderTarget']=self.rt
|
||||
app=mods['app'];app.DIK_X=45;app.IsPressed=lambda k:k in self.keys
|
||||
mods['chat'].CHAT_TYPE_INFO=1;mods['chat'].AppendChat=lambda t,m:self.chat.append(m)
|
||||
mods['game'].GameWindow=type('GameWindow',(),dict(Open=lambda s:None,Close=lambda s:None))
|
||||
grp=mods['grp'];grp.SetColor=grp.RenderBar=grp.RenderBox=lambda *a:None
|
||||
it=mods['item']
|
||||
for k,v in dict(ITEM_TYPE_WEAPON=1,ITEM_TYPE_ARMOR=2,ITEM_TYPE_COSTUME=28,WEAPON_ARROW=6,ARMOR_BODY=0,
|
||||
COSTUME_TYPE_BODY=0,COSTUME_TYPE_HAIR=1,ITEM_ANTIFLAG_FEMALE=0x01,ITEM_ANTIFLAG_MALE=0x02,
|
||||
ITEM_ANTIFLAG_WARRIOR=0x04,ITEM_ANTIFLAG_ASSASSIN=0x08,ITEM_ANTIFLAG_SURA=0x10,
|
||||
ITEM_ANTIFLAG_SHAMAN=0x20).items():setattr(it,k,v)
|
||||
def select(v):self.selected=ITEMS.get(v,ITEMS[27001])
|
||||
it.SelectItem=select;it.GetItemType=lambda:self.selected[0];it.GetItemSubType=lambda:self.selected[1]
|
||||
it.IsAntiFlag=lambda f:bool(self.selected[2]&f)
|
||||
mods['player'].GetRace=lambda:self.race
|
||||
self.bag={3:11299,4:27001}
|
||||
mods['player'].GetItemIndex=lambda cell:self.bag.get(cell,0)
|
||||
mods['ui'].Window=Widget
|
||||
mods['wndMgr'].GetMousePosition=lambda:self.mouse
|
||||
mods['mt_biolog'].Action=Action;mods['mt_biolog'].L=lambda t:t
|
||||
mods['mt_biolog'].Text=lambda p,x,y,t='',c=0:self.text(t)
|
||||
self.inventoryClicks=[];self.shopClicks=[]
|
||||
mods['uiInventory'].InventoryWindow=type('InventoryWindow',(),dict(
|
||||
SelectItemSlot=lambda s,slot:self.inventoryClicks.append(slot),
|
||||
_InventoryWindow__InventoryLocalSlotPosToGlobalSlotPos=lambda s,slot:slot+0))
|
||||
mods['uiShop'].ShopDialog=type('ShopDialog',(),dict(SelectItemSlot=lambda s,slot:self.shopClicks.append(slot),
|
||||
_ShopDialog__GetRealIndex=lambda s,slot:40+slot))
|
||||
mods['shop'].GetItemID=lambda i:{41:12049,42:27001}.get(i,0)
|
||||
self.previous={n:sys.modules.get(n) for n in mods}
|
||||
sys.modules.update(mods)
|
||||
self.ns={'__name__':'mt_preview'};exec(script('ModelPreviewScript.inc'),self.ns)
|
||||
self.game=mods['game'];self.gw=self.game.GameWindow();self.game.GameWindow.Open(self.gw)
|
||||
self.w=self.ns['g_window']
|
||||
|
||||
def text(self,t):
|
||||
w=Widget();w.text=t;return w
|
||||
|
||||
def tearDown(self):
|
||||
for name,value in self.previous.items():
|
||||
if value is None:sys.modules.pop(name,None)
|
||||
else:sys.modules[name]=value
|
||||
|
||||
def test_item_spec(self):
|
||||
spec=self.ns['ItemSpec']
|
||||
self.assertEqual(spec(11299),('weapon',[0,4,5,1,2,6,7,3]))
|
||||
self.assertEqual(spec(11499),('weapon',[0,4])) # warrior only
|
||||
self.assertEqual(spec(12049),('armor',[5,1])) # assassin only
|
||||
self.assertEqual(spec(41001),('armor',[0,5,2,7])) # anti-female costume
|
||||
self.assertEqual(spec(45001),('hair',[4,1,6,3]))
|
||||
for vnum in (8000,27001,11259):self.assertIsNone(spec(vnum)) # arrow, potion, helmet
|
||||
self.assertTrue(self.ns['Previewable']('mob',101));self.assertFalse(self.ns['Previewable']('item',27001))
|
||||
|
||||
def test_item_preview_races_and_reset(self):
|
||||
self.race=2
|
||||
self.assertTrue(self.ns['Preview']('item',11299));self.assertTrue(self.w.IsShow())
|
||||
self.assertEqual(self.rt.live,{1});self.assertIn(('Create',1,300,330),self.rt.calls)
|
||||
self.assertEqual(self.rt.models[1],2);self.assertEqual(self.rt.named('SetWeapon')[-1],('SetWeapon',1,11299))
|
||||
self.assertEqual(self.w.raceName.text,'修罗(男)');self.assertEqual(self.rt.named('Zoom'),[('Zoom',1,1)]*2)
|
||||
self.w.nextRace.event();self.assertEqual(self.rt.models[1],6);self.assertEqual(self.w.raceName.text,'修罗(女)')
|
||||
self.w.prevRace.event();self.w.prevRace.event();self.assertEqual(self.rt.models[1],1)
|
||||
# The player's own race is not allowed: the first allowed race is shown, and race buttons need two races.
|
||||
self.ns['Preview']('item',41001);self.assertEqual(self.rt.models[1],2)
|
||||
self.assertEqual(self.rt.named('SetArmor')[-1],('SetArmor',1,41001))
|
||||
self.ns['Preview']('item',12049);self.assertEqual(self.rt.models[1],5)
|
||||
self.ns['Preview']('item',45001);self.assertEqual(self.rt.models[1],4)
|
||||
self.assertEqual(self.rt.named('SetHair')[-1],('SetHair',1,45001))
|
||||
self.assertTrue(self.w.nextRace.enabled)
|
||||
self.ns['Preview']('item',11499);self.race=0;self.ns['Preview']('item',11499)
|
||||
self.assertEqual(self.rt.models[1],0)
|
||||
self.assertFalse(self.ns['Preview']('item',27001));self.assertEqual(self.chat,['该物品没有可预览的模型'])
|
||||
self.assertEqual(self.rt.models[1],0) # the window keeps what it showed
|
||||
|
||||
def test_mob_motions_zoom_turn(self):
|
||||
self.rt.motionless.add(34)
|
||||
self.assertTrue(self.ns['Preview']('mob',101));self.assertEqual(self.rt.models[1],101)
|
||||
self.assertFalse(self.w.prevRace.IsShow());self.assertTrue(all(b.IsShow() for b in self.w.motions))
|
||||
self.w.motions[1].event();self.assertEqual(self.rt.named('SetMotionIndex')[-1],('SetMotionIndex',1,13))
|
||||
self.w.motions[5].event();self.assertEqual(self.w.note.text,'该怪物没有此动作')
|
||||
self.w.motions[0].event();self.assertEqual(self.w.note.text,'')
|
||||
zoom_in,zoom_out,spin,reset=[b.event for b in self.w.controls]
|
||||
for _ in range(20):zoom_in()
|
||||
self.assertEqual(self.w.zoom,-8);self.assertEqual(len(self.rt.named('Zoom')),8)
|
||||
for _ in range(30):zoom_out()
|
||||
self.assertEqual(self.w.zoom,12);self.assertEqual(len(self.rt.named('Zoom')),28)
|
||||
spin();self.assertEqual(self.rt.named('SetRotation')[-1],('SetRotation',1,True));self.assertEqual(self.w.controls[2].text,'停止旋转')
|
||||
# Drag: absolute yaw/pitch from where the press started, pitch clamped.
|
||||
view=self.w.view;self.mouse=(100,100);view.OnMouseLeftButtonDown()
|
||||
self.mouse=(160,1100);view.OnUpdate()
|
||||
self.assertEqual(self.rt.named('RotateEyeAroundTarget')[-1],('RotateEyeAroundTarget',1,60.,30.))
|
||||
view.OnMouseLeftButtonUp();self.mouse=(0,0);view.OnUpdate()
|
||||
self.assertEqual(self.rt.named('RotateEyeAroundTarget')[-1],('RotateEyeAroundTarget',1,60.,30.))
|
||||
reset();self.assertEqual((self.w.yaw,self.w.pitch,self.w.zoom),(0.,0.,0))
|
||||
self.assertEqual(self.rt.named('SetRotation')[-1],('SetRotation',1,True)) # spin survives the reset
|
||||
self.rt.missing.add(999);self.ns['Preview']('mob',999);self.assertEqual(self.w.note.text,'该怪物没有模型')
|
||||
zoom_in();self.assertEqual(self.w.zoom,0)
|
||||
|
||||
def test_view_draws_target(self):
|
||||
self.ns['Preview']('mob',101);self.w.view.pos=(20,70);self.w.view.OnRender()
|
||||
self.assertEqual(self.rt.named('RenderTexture')[-1],('RenderTexture',1,20,70,320,400))
|
||||
|
||||
def test_no_leak_on_reopen_and_close(self):
|
||||
for _ in range(5):
|
||||
self.ns['Preview']('item',11299);self.assertEqual(self.rt.live,{1})
|
||||
self.w.closeButton.event();self.assertEqual(self.rt.live,set());self.assertFalse(self.w.IsShow())
|
||||
self.ns['Preview']('mob',101);self.w.OnPressEscapeKey();self.assertEqual(self.rt.live,set())
|
||||
self.ns['Preview']('mob',101)
|
||||
self.game.GameWindow.Close(self.gw)
|
||||
self.assertIsNone(self.ns['g_window']);self.assertEqual(self.rt.live,set());self.assertEqual(self.rt.calls[-1],('Destroy',))
|
||||
self.assertFalse(self.ns['Preview']('mob',101))
|
||||
|
||||
def test_inventory_and_shop_entries(self):
|
||||
inv=sys.modules['uiInventory'].InventoryWindow()
|
||||
inv.SelectItemSlot(3);self.assertEqual(self.inventoryClicks,[3]);self.assertFalse(self.w.IsShow())
|
||||
self.keys.add(45)
|
||||
inv.SelectItemSlot(3);self.assertEqual(self.inventoryClicks,[3]);self.assertTrue(self.w.IsShow())
|
||||
inv.SelectItemSlot(4);inv.SelectItemSlot(9);self.assertEqual(self.inventoryClicks,[3,4,9]) # not wearable, empty
|
||||
dlg=sys.modules['uiShop'].ShopDialog()
|
||||
dlg.SelectItemSlot(1);self.assertEqual(self.shopClicks,[]);self.assertEqual(self.rt.named('SetArmor')[-1],('SetArmor',1,12049))
|
||||
dlg.SelectItemSlot(2);self.assertEqual(self.shopClicks,[2])
|
||||
self.keys.clear();dlg.SelectItemSlot(1);self.assertEqual(self.shopClicks,[2,1])
|
||||
|
||||
def test_wiki_links(self):
|
||||
wiki=(ROOT/'src/platform/ScriptLib/WikiScript.inc').read_text()
|
||||
self.assertIn("(u'模型预览',('preview','item',vnum))",wiki);self.assertIn("(u'模型预览',('preview','mob',vnum))",wiki)
|
||||
self.assertIn("mt_preview.Preview(vnum[1],vnum[2])",wiki)
|
||||
boot=(ROOT/'src/platform/ScriptLib/PythonBoot.cpp').read_text()
|
||||
self.assertLess(boot.index('install_script_module("mt_mobile_inventory"'),boot.index('install_script_module("mt_preview"'))
|
||||
self.assertLess(boot.index('install_script_module("mt_biolog"'),boot.index('install_script_module("mt_preview"'))
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
Reference in New Issue
Block a user