Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d01a7a754 | ||
|
|
b9af78ef64 | ||
|
|
a1b63c0afb | ||
|
|
1412806ee9 | ||
|
|
4d7306792e | ||
|
|
0d2a9ebe36 | ||
|
|
178fcf48fe | ||
|
|
f266072f67 | ||
|
|
90f213c4cc | ||
|
|
d11d46c469 | ||
|
|
8164c02709 | ||
|
|
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.
|
||||
@@ -356,7 +356,7 @@ def build(en: Path, zh: Path, output: Path, client_base: Path,
|
||||
[str(proto_tool), str(en / "item_proto"), str(en / "mob_proto"),
|
||||
str(item_names), str(mob_names), str(locale_dir / "item_proto"),
|
||||
str(locale_dir / "mob_proto")] + ([str(limits)] if limits.is_file() else [])
|
||||
+ ([str(names_dir / "item_stones_server.tsv")] if (names_dir / "item_stones_server.tsv").is_file() else []),
|
||||
+ ([str(names_dir / "item_stones_server.tsv")] if (names_dir / "item_stones_server.tsv").is_file() else [])
|
||||
# item_shapes_server.tsv follows the stone overlay (positional): size and flags of new-use items.
|
||||
+ ([str(names_dir / "item_shapes_server.tsv")] if (names_dir / "item_shapes_server.tsv").is_file() else []),
|
||||
check=True, capture_output=True, text=True,
|
||||
@@ -397,7 +397,10 @@ def build_biolog(output: Path, pack_tool: Path):
|
||||
root = REPO / "tools/server/biolog"
|
||||
ava = REPO.parent / "AVA2"
|
||||
archive = ava / "2. Backup SF+SRC and SQL/FILES+SRC.tgz"
|
||||
if not archive.is_file():
|
||||
extracted = ava / "TEST/2. Backup SF+SRC and SQL/home/srv1/db/item_proto.txt"
|
||||
if not archive.is_file() and extracted.is_file():
|
||||
ava = ava / "TEST" # the reference unpacked under AVA2/TEST
|
||||
elif not archive.is_file():
|
||||
raise ValueError("AVA2 biology requires the local FILES+SRC.tgz reference")
|
||||
proto_tool = tool("biolog_proto_patch")
|
||||
if not proto_tool.is_file():
|
||||
@@ -405,11 +408,14 @@ def build_biolog(output: Path, pack_tool: Path):
|
||||
stage = output.parent / "biolog"
|
||||
stage.mkdir(parents=True, exist_ok=True)
|
||||
proto = stage / "ava2-item-proto.txt"
|
||||
with tarfile.open(archive, "r:gz") as source:
|
||||
member = source.extractfile("home/srv1/db/item_proto.txt")
|
||||
if member is None:
|
||||
raise ValueError("AVA2 item table missing")
|
||||
proto.write_bytes(member.read())
|
||||
if not archive.is_file():
|
||||
shutil.copyfile(extracted, proto)
|
||||
else:
|
||||
with tarfile.open(archive, "r:gz") as source:
|
||||
member = source.extractfile("home/srv1/db/item_proto.txt")
|
||||
if member is None:
|
||||
raise ValueError("AVA2 item table missing")
|
||||
proto.write_bytes(member.read())
|
||||
spec = importlib.util.spec_from_file_location("biolog_assets", root / "make_assets.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
@@ -425,6 +431,36 @@ def build_biolog(output: Path, pack_tool: Path):
|
||||
return {"stages": 13, "item_templates": 29, "pack": "biolog", "first_priority": True}
|
||||
|
||||
|
||||
def build_ava2_items(output: Path, pack_tool: Path):
|
||||
"""AVA2 drop items missing on the server, layered over the biolog item table (see tools/server/ava2-items)."""
|
||||
import importlib.util
|
||||
root = REPO / "tools/server/ava2-items"
|
||||
ava = REPO.parent / "AVA2/TEST"
|
||||
reader = REPO.parent / "metin2-server/src/server/db/src/ProtoReader.cpp"
|
||||
if not (ava / "1. Client DEV/pack_maker").is_dir() or not reader.is_file():
|
||||
raise ValueError("AVA2 items need ../AVA2/TEST (pack_maker + server db) and ../metin2-server")
|
||||
proto_tool = tool("item_proto_patch")
|
||||
if not proto_tool.is_file():
|
||||
raise ValueError("build item_proto_patch before generating the Chinese client")
|
||||
stage = output.parent / "ava2-items"
|
||||
spec = importlib.util.spec_from_file_location("ava2_items", root / "make_items.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
# biolog's pack carries the current locale/zh item tables; this pack must replace them, so it goes first.
|
||||
report = module.prepare(ava, reader, output.parent / "biolog/assets/locale/zh", stage,
|
||||
proto_tool.resolve(), pack_tool.resolve(), wrap_description)
|
||||
for extension in ("eix", "epk"):
|
||||
shutil.copy2(stage / ("ava2items." + extension), output / "pack" / ("ava2items." + extension))
|
||||
index = output / "pack/Index"
|
||||
lines = index.read_bytes().replace(b"\r\n", b"\n").splitlines()
|
||||
if b"ava2items" in lines:
|
||||
del lines[lines.index(b"ava2items") - 1:lines.index(b"ava2items") + 1]
|
||||
at = lines.index(b"biolog") - 1
|
||||
lines[at:at] = [b"*", b"ava2items"]
|
||||
index.write_bytes(b"\r\n".join(lines) + b"\r\n")
|
||||
return {"items": len(report["kept"]) + len(report["locked"]), "kept": len(report["kept"]),
|
||||
"locked": len(report["locked"]), "assets": report["assets"], "pack": "ava2items"}
|
||||
|
||||
|
||||
def build_wiki(output: Path, pack_tool: Path):
|
||||
"""A versioned public server catalogue; original script and proto packs stay intact."""
|
||||
@@ -466,6 +502,7 @@ def main():
|
||||
args.proto_tool, args.name_tables, args.pack_tool)
|
||||
report = json.loads(report_path.read_text())
|
||||
report["biolog"] = build_biolog(args.output, args.pack_tool)
|
||||
report["ava2_items"] = build_ava2_items(args.output, args.pack_tool)
|
||||
report["wiki"] = build_wiki(args.output, args.pack_tool)
|
||||
report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2)+"\n", encoding="utf-8")
|
||||
print(f"Built {args.output / 'pack/locale_zh.eix'} (.epk), registered in pack/Index")
|
||||
|
||||
+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,28 @@
|
||||
// PORT: item bonuses added after 40250 (AVA2 numbering, server common/length.h). The packed uitooltip.py
|
||||
// only knows 40250's applies; add the new ones to its AFFECT_DICT so item and socket tooltips show them.
|
||||
static const char c_szAppliesScript[] = R"PY(# -*- coding: utf-8 -*-
|
||||
import item
|
||||
import player
|
||||
import uiToolTip
|
||||
|
||||
L = player.AutoHuntLocalize
|
||||
TEXTS = (
|
||||
("APPLY_ATTBONUS_STONE", "对陨石伤害 +%d%%"),
|
||||
("APPLY_ATTBONUS_BOSS", "对 Boss 伤害 +%d%%"),
|
||||
("APPLY_ENCHANT_ELEC", "对雷属性怪物伤害 +%d%%"),
|
||||
("APPLY_ENCHANT_FIRE", "对火属性怪物伤害 +%d%%"),
|
||||
("APPLY_ENCHANT_ICE", "对冰属性怪物伤害 +%d%%"),
|
||||
("APPLY_ENCHANT_WIND", "对风属性怪物伤害 +%d%%"),
|
||||
("APPLY_ENCHANT_EARTH", "对土属性怪物伤害 +%d%%"),
|
||||
("APPLY_ENCHANT_DARK", "对暗属性怪物伤害 +%d%%"),
|
||||
)
|
||||
|
||||
|
||||
def Text(text):
|
||||
return lambda value: L(text % value)
|
||||
|
||||
|
||||
for name, text in TEXTS:
|
||||
if hasattr(item, name):
|
||||
uiToolTip.ItemToolTip.AFFECT_DICT[getattr(item, name)] = Text(text)
|
||||
)PY";
|
||||
@@ -0,0 +1,34 @@
|
||||
// PORT: armor shapes added after 40250 (AVA2's dragon armor 21210+, item value3 = 32). The race .msm files are
|
||||
// read by bare name, which CEterPackManager always serves from the root pack, so a patch pack cannot replace
|
||||
// them; register the extra shapes after each LoadLocalRaceData instead, as the AVA2 msm groups would. Models
|
||||
// ship in the ava2items pack (metin2-client tools/server/ava2-items, ARMOR_SHAPES / RACE_PATHS).
|
||||
static const char c_szArmorShapesScript[] = R"PY(# -*- coding: utf-8 -*-
|
||||
import chrmgr
|
||||
|
||||
SHAPES = {32: "%s_7_1"}
|
||||
# ShapeData PathName of each race msm (male, female).
|
||||
PATHS = {"warrior": ("pc", "pc2"), "assassin": ("pc2", "pc"), "sura": ("pc", "pc2"), "shaman": ("pc2", "pc")}
|
||||
|
||||
_load = chrmgr.LoadLocalRaceData
|
||||
|
||||
|
||||
def Register(name):
|
||||
stem = name.lower().split(".")[0]
|
||||
race, _, sex = stem.partition("_")
|
||||
if race not in PATHS or sex not in ("m", "w"):
|
||||
return
|
||||
path = "d:/ymir work/%s/%s/" % (PATHS[race][sex == "w"], race)
|
||||
for shape, model in SHAPES.items():
|
||||
model = path + model % race
|
||||
chrmgr.SetShapeModel(shape, model + ".gr2")
|
||||
chrmgr.AppendShapeSkin(shape, 0, model + ".dds", model + ".dds")
|
||||
|
||||
|
||||
def LoadLocalRaceData(name):
|
||||
result = _load(name)
|
||||
Register(name)
|
||||
return result
|
||||
|
||||
|
||||
chrmgr.LoadLocalRaceData = LoadLocalRaceData
|
||||
)PY";
|
||||
@@ -0,0 +1,364 @@
|
||||
// PORT: battle pass (战令), from AVA2's uibattlepassex.py. The server runs monthly seasons of up to ten missions
|
||||
// counted from real events (kills, fishing, refines, levels, NPC trades, play time); each mission has a reward and
|
||||
// a final reward follows once all are done. This window asks with /mt_bp, shows progress live while open and
|
||||
// claims rewards. Entry points: F6 and a "战令" button by the minimap. Protocol: metin2-server
|
||||
// docs/FEATURE-BATTLEPASS.md.
|
||||
static const char c_szBattlePassScript[] = R"PY(# -*- coding: utf-8 -*-
|
||||
import app
|
||||
import chat
|
||||
import game
|
||||
import grp
|
||||
import item
|
||||
import net
|
||||
import nonplayer
|
||||
import ui
|
||||
import weakref
|
||||
from mt_biolog import Action, Text, L
|
||||
|
||||
g_window=None
|
||||
g_entry=None
|
||||
SEND_GAP=.4
|
||||
TIMEOUT=3.
|
||||
PAGE=5
|
||||
ROW=56
|
||||
WIDTH=620
|
||||
MAX_SLOT=10
|
||||
# type: (name without sub, name with a sub vnum: kind of the vnum)
|
||||
TYPES={1:('击杀怪物','击杀 ','mob'),2:('击杀 Boss','击杀 Boss ','mob'),3:('摧毁陨石','摧毁 ','mob'),4:('累计在线',None,None),
|
||||
5:('钓鱼','钓到 ','item'),6:('成功强化装备','强化出 ','item'),7:('提升等级',None,None),8:('在 NPC 商店消费金币',None,None),
|
||||
9:(None,'向 NPC 出售 ','item')}
|
||||
ERRORS={'loading':'数据加载中,请稍后再试','slot':'没有这个任务','claimed':'奖励已经领取过了','incomplete':'任务尚未完成',
|
||||
'space':'背包空间不足,请整理后再领取','config':'奖励配置有误,请联系管理员','db':'服务器繁忙,请稍后再试',
|
||||
'rate':'操作过快,请稍后再试','invalid':'参数无效'}
|
||||
|
||||
|
||||
def Number(text,low=0,high=10**12):
|
||||
if not text.isdigit() or len(text)>13:raise ValueError(text)
|
||||
value=int(text)
|
||||
if not low<=value<=high:raise ValueError(text)
|
||||
return value
|
||||
|
||||
|
||||
def Rewards(words):
|
||||
if len(words)%2:raise ValueError(words)
|
||||
return [(Number(words[i],1,2**32-1),Number(words[i+1],1,10000)) for i in range(0,len(words),2)]
|
||||
|
||||
|
||||
def Grouped(value):
|
||||
text=str(int(value));parts=[]
|
||||
while len(text)>3:parts.insert(0,text[-3:]);text=text[:-3]
|
||||
return ','.join([text]+parts)
|
||||
|
||||
|
||||
def Amount(kind,value,target):
|
||||
# Play time is counted in seconds and shown in hours, or in minutes for a target under an hour.
|
||||
if kind==4:
|
||||
unit=3600. if target>=3600 else 60.
|
||||
amount=value/unit
|
||||
return ('%d'%amount) if amount==int(amount) else ('%.1f'%amount)
|
||||
return Grouped(value)
|
||||
|
||||
|
||||
def Unit(kind,target):
|
||||
if kind!=4:return ''
|
||||
return ' 小时' if target>=3600 else ' 分钟'
|
||||
|
||||
|
||||
def SeasonName(season):
|
||||
year,month=divmod(season,100)
|
||||
if 2000<=year<=2999 and 1<=month<=12:return '%d 年 %d 月'%(year,month)
|
||||
return '第 %d 期'%season
|
||||
|
||||
|
||||
def Left(seconds):
|
||||
seconds=int(seconds)
|
||||
if seconds<=0:return '已结束'
|
||||
days,rest=divmod(seconds,86400);hours,rest=divmod(rest,3600)
|
||||
if days:return '剩余 %d 天 %d 小时'%(days,hours)
|
||||
return '剩余 %d 小时 %d 分'%(hours,max(1,rest//60))
|
||||
|
||||
|
||||
class Pass(object):
|
||||
# Protocol state only. send(text) posts a chat command, now() is seconds.
|
||||
def __init__(self,send,now,listener=None):
|
||||
self.send=send;self.now=now;self.listener=listener
|
||||
self.waiting=None;self.nextSend=0;self.unsupported=False
|
||||
self.Clear()
|
||||
|
||||
def Clear(self):
|
||||
self.season=None;self.missions={};self.final=None;self.reading=None;self.none=False;self.error=None;self.claiming=None
|
||||
|
||||
def Ready(self):
|
||||
return self.waiting is None or self.now()>=self.waiting
|
||||
|
||||
def Post(self,text,expect=True):
|
||||
now=self.now()
|
||||
if now<self.nextSend:return False
|
||||
self.send(text);self.nextSend=now+SEND_GAP
|
||||
if expect:self.waiting=now+TIMEOUT
|
||||
return True
|
||||
|
||||
def Query(self):
|
||||
self.Tick()
|
||||
if self.waiting is not None:return False
|
||||
return self.Post('/mt_bp 1 info')
|
||||
|
||||
def Claim(self,slot):
|
||||
if self.claiming is not None or not self.season:return False
|
||||
if self.Post('/mt_bp 1 claim %d'%slot):self.claiming=slot;self.error=None;return True
|
||||
return False
|
||||
|
||||
def Close(self):
|
||||
self.send('/mt_bp 1 close')
|
||||
|
||||
def Tick(self):
|
||||
if self.waiting is not None and self.now()>=self.waiting:
|
||||
self.waiting=None;self.claiming=None
|
||||
if self.season is None:self.unsupported=True
|
||||
self.Notify('timeout')
|
||||
|
||||
def SecondsLeft(self):
|
||||
if not self.season:return 0
|
||||
_,_,end,now,at=self.season
|
||||
return end-(now+(self.now()-at))
|
||||
|
||||
def Notify(self,kind,value=None):
|
||||
if self.listener:self.listener(kind,value)
|
||||
|
||||
def Receive(self,line):
|
||||
if not line.startswith('MTBP1 '):return False
|
||||
words=line.split()
|
||||
try:self.Parse(words[1],words[2:])
|
||||
except (ValueError,IndexError):self.reading=None
|
||||
return True
|
||||
|
||||
def Parse(self,kind,args):
|
||||
if kind=='season' and len(args)==5:
|
||||
seasonId,start,end,now=[Number(w,0,2**32-1) for w in args[:4]];count=Number(args[4],1,MAX_SLOT)
|
||||
self.reading=dict(season=(seasonId,start,end,now,self.now()),count=count,missions={},final=None)
|
||||
elif kind=='mission' and self.reading is not None and len(args)>=6:
|
||||
slot=Number(args[0],1,MAX_SLOT);kind=Number(args[1],1,9)
|
||||
self.reading['missions'][slot]=dict(slot=slot,type=kind,sub=Number(args[2],0,2**32-1),target=Number(args[3],1),
|
||||
value=Number(args[4]),claimed=Number(args[5],0,1)==1,rewards=Rewards(args[6:]))
|
||||
elif kind=='final' and self.reading is not None and len(args)>=4:
|
||||
self.reading['final']=dict(claimed=Number(args[0],0,1)==1,all=Number(args[1],0,1)==1,rewards=Rewards(args[2:]))
|
||||
elif kind=='end' and not args and self.reading is not None:
|
||||
r=self.reading;self.reading=None
|
||||
if len(r['missions'])!=r['count'] or r['final'] is None:return
|
||||
self.season=r['season'];self.missions=r['missions'];self.final=r['final'];self.none=False
|
||||
self.Answered('info')
|
||||
elif kind=='none' and not args:
|
||||
self.Clear();self.none=True;self.Answered('none')
|
||||
elif kind=='progress' and len(args)==2:
|
||||
slot=Number(args[0],1,MAX_SLOT);m=self.missions.get(slot)
|
||||
if m:m['value']=min(m['target'],Number(args[1]));self.Notify('progress',slot)
|
||||
elif kind=='done' and len(args)==1:
|
||||
slot=Number(args[0],1,MAX_SLOT);m=self.missions.get(slot)
|
||||
if m:m['value']=m['target']
|
||||
self.Notify('done',slot)
|
||||
elif kind=='claimed' and len(args)==1:
|
||||
slot=Number(args[0],0,MAX_SLOT)
|
||||
if slot:
|
||||
if slot in self.missions:self.missions[slot]['claimed']=True
|
||||
elif self.final:self.final['claimed']=True
|
||||
self.claiming=None;self.Notify('claimed',slot) # a full info follows
|
||||
elif kind=='error' and len(args)==1 and args[0] in ERRORS:
|
||||
self.error=args[0];self.claiming=None;self.Answered('error')
|
||||
|
||||
def Answered(self,kind):
|
||||
self.waiting=None;self.unsupported=False;self.Notify(kind)
|
||||
|
||||
def AllDone(self):
|
||||
return bool(self.missions) and all(m['value']>=m['target'] for m in self.missions.values())
|
||||
|
||||
|
||||
def ItemName(vnum):
|
||||
try:
|
||||
item.SelectItem(vnum);name=item.GetItemName()
|
||||
except Exception:name=''
|
||||
return name if name else L('物品 %d'%vnum)
|
||||
|
||||
|
||||
def MobName(vnum):
|
||||
try:name=nonplayer.GetMonsterName(vnum)
|
||||
except Exception:name=''
|
||||
return name if name else L('怪物 %d'%vnum)
|
||||
|
||||
|
||||
def MissionName(m):
|
||||
# Locale text: the utf-8 parts go through L, game names are already in the locale's code page.
|
||||
plain,prefix,kind=TYPES.get(m['type'],('任务',None,None))
|
||||
if m['sub'] and prefix:return L(prefix)+(MobName(m['sub']) if kind=='mob' else ItemName(m['sub']))
|
||||
return L(plain or '任务')
|
||||
|
||||
|
||||
def RewardText(rewards):
|
||||
return L(',').join([ItemName(vnum)+(' x%d'%count) for vnum,count in rewards])
|
||||
|
||||
|
||||
class BattlePassWindow(ui.Window):
|
||||
def __init__(self,state):
|
||||
ui.Window.__init__(self);self.SetWindowName('BattlePassWindow');self.AddFlag('float');self.AddFlag('movable')
|
||||
self.state=state;self.page=0
|
||||
self.SetSize(WIDTH,74+PAGE*ROW+ROW+20+56)
|
||||
self.heading=Text(self,18,18,'战令',0xffffd68b)
|
||||
self.season=Text(self,80,18,'',0xffc3b7a1)
|
||||
self.closeButton=Action(self,WIDTH-76,6,64,'关闭',self.Hide)
|
||||
self.rows=[]
|
||||
for i in range(PAGE):
|
||||
y=64+i*ROW
|
||||
self.rows.append(dict(name=Text(self,18,y+4,''),value=Text(self,330,y+4,''),
|
||||
reward=Text(self,18,y+26,'',0xffb3aa98),button=Action(self,WIDTH-118,y,100,'领取',lambda i=i:self.ClaimRow(i))))
|
||||
y=64+PAGE*ROW+8
|
||||
self.finalName=Text(self,18,y+4,'最终奖励',0xffffd68b)
|
||||
self.finalState=Text(self,330,y+4,'')
|
||||
self.finalReward=Text(self,18,y+26,'',0xffb3aa98)
|
||||
self.finalButton=Action(self,WIDTH-118,y,100,'领取',lambda:self.state.Claim(0) and self.Refresh())
|
||||
y=self.GetHeight()-56
|
||||
self.prev=Action(self,18,y,80,'上一页',lambda:self.Turn(-1))
|
||||
self.pageText=Text(self,140,y+24,'');self.pageText.SetHorizontalAlignCenter()
|
||||
self.next=Action(self,182,y,80,'下一页',lambda:self.Turn(1))
|
||||
self.note=Text(self,280,y+16,'',0xffc3b7a1)
|
||||
self.second=-1
|
||||
self.Hide()
|
||||
|
||||
def Open(self):
|
||||
self.state.error=None;self.state.Query();self.Refresh();self.SetCenterPosition();ui.Window.Show(self);self.SetTop()
|
||||
|
||||
def Toggle(self):
|
||||
if self.IsShow():self.Hide()
|
||||
else:self.Open()
|
||||
|
||||
def Hide(self):
|
||||
state=self.__dict__.get('state') # ui.Window.__init__ hides the window before state is set
|
||||
if self.IsShow() and state and state.season:state.Close()
|
||||
ui.Window.Hide(self)
|
||||
|
||||
def Slots(self):
|
||||
return sorted(self.state.missions)
|
||||
|
||||
def Pages(self):
|
||||
return max(1,(len(self.Slots())+PAGE-1)//PAGE)
|
||||
|
||||
def Turn(self,delta):
|
||||
self.page=max(0,min(self.Pages()-1,self.page+delta));self.Refresh()
|
||||
|
||||
def ClaimRow(self,i):
|
||||
slots=self.Slots()[self.page*PAGE:]
|
||||
if i<len(slots) and self.state.Claim(slots[i]):self.Refresh()
|
||||
|
||||
def Refresh(self):
|
||||
s=self.state;self.page=min(self.page,self.Pages()-1)
|
||||
slots=self.Slots()[self.page*PAGE:(self.page+1)*PAGE]
|
||||
for i,row in enumerate(self.rows):
|
||||
widgets=(row['name'],row['value'],row['reward'],row['button'])
|
||||
if i>=len(slots):
|
||||
for w in widgets:w.Hide()
|
||||
continue
|
||||
for w in widgets:w.Show()
|
||||
m=s.missions[slots[i]];done=m['value']>=m['target']
|
||||
row['name'].SetText(MissionName(m))
|
||||
row['value'].SetText(Amount(m['type'],m['value'],m['target'])+' / '+Amount(m['type'],m['target'],m['target'])+L(Unit(m['type'],m['target'])))
|
||||
row['value'].SetPackedFontColor(0xff9fe08b if done else 0xffe8dec8)
|
||||
row['reward'].SetText(L('奖励:')+RewardText(m['rewards']) if m['rewards'] else L('无奖励'))
|
||||
if m['claimed']:row['button'].SetText('已领取')
|
||||
elif s.claiming==m['slot']:row['button'].SetText('领取中')
|
||||
else:row['button'].SetText('领取' if done else '未完成')
|
||||
row['button'].SetEnabled(done and not m['claimed'] and bool(m['rewards']) and s.claiming is None)
|
||||
f=s.final
|
||||
for w in (self.finalName,self.finalState,self.finalReward,self.finalButton):
|
||||
if f:w.Show()
|
||||
else:w.Hide()
|
||||
if f:
|
||||
ready=s.AllDone()
|
||||
self.finalState.SetText(L('已领取' if f['claimed'] else '全部任务完成' if ready else '完成全部任务后可领取'))
|
||||
self.finalReward.SetText(L('奖励:')+RewardText(f['rewards']))
|
||||
self.finalButton.SetText('已领取' if f['claimed'] else '领取中' if s.claiming==0 else '领取')
|
||||
self.finalButton.SetEnabled(ready and not f['claimed'] and s.claiming is None)
|
||||
many=self.Pages()>1
|
||||
for w in (self.prev,self.next,self.pageText):
|
||||
if many and s.season:w.Show()
|
||||
else:w.Hide()
|
||||
self.prev.SetEnabled(self.page>0);self.next.SetEnabled(self.page<self.Pages()-1)
|
||||
self.pageText.SetText('%d / %d'%(self.page+1,self.Pages()))
|
||||
if s.season:self.season.SetText(L(SeasonName(s.season[0])+' '+Left(s.SecondsLeft())))
|
||||
else:self.season.SetText('')
|
||||
if s.unsupported:note='!服务器尚未启用战令'
|
||||
elif s.error:note='!'+ERRORS[s.error]
|
||||
elif s.none:note='当前没有进行中的战令赛季'
|
||||
elif not s.season:note='正在查询……'
|
||||
elif s.claiming is not None:note='正在领取……'
|
||||
else:note='击杀、钓鱼、强化等都会自动计入进度'
|
||||
self.note.SetText(L(note))
|
||||
|
||||
def OnUpdate(self):
|
||||
self.state.Tick()
|
||||
second=int(app.GetTime())
|
||||
if second!=self.second:self.second=second;self.Refresh()
|
||||
|
||||
def OnRender(self):
|
||||
x,y=self.GetGlobalPosition();w=self.GetWidth()
|
||||
grp.SetColor(0xf51b1916);grp.RenderBar(x,y,w,self.GetHeight());grp.SetColor(0xff9b7b48);grp.RenderBox(x,y,w,self.GetHeight())
|
||||
grp.SetColor(0xff4a3d2a)
|
||||
for i in range(PAGE+1):grp.RenderLine(x+12,y+60+i*ROW,w-24,0)
|
||||
|
||||
def OnPressEscapeKey(self):self.Hide();return True
|
||||
|
||||
def Destroy(self):
|
||||
self.Hide();self.closeButton.event=self.finalButton.event=self.prev.event=self.next.event=None
|
||||
for row in self.rows:row['button'].event=None
|
||||
self.state=None
|
||||
|
||||
|
||||
class Entry(Action):
|
||||
def __init__(self,window):
|
||||
Action.__init__(self,None,0,0,64,'战令',Toggle)
|
||||
self.minimap=weakref.proxy(window.interface.wndMiniMap);self.SetWindowName('BattlePassEntry');self.OnUpdate()
|
||||
|
||||
def OnUpdate(self):
|
||||
x,y=self.minimap.GetGlobalPosition();self.SetPosition(max(4,x-68),y+460)
|
||||
|
||||
|
||||
def OnPass(kind,value):
|
||||
s=g_pass
|
||||
if kind=='done':
|
||||
m=s.missions.get(value)
|
||||
name=MissionName(m) if m else L('一项任务')
|
||||
chat.AppendChat(chat.CHAT_TYPE_INFO,L('战令任务完成:')+name+L(',按 F6 领取奖励'))
|
||||
elif kind=='claimed':chat.AppendChat(chat.CHAT_TYPE_INFO,L('战令奖励已放入背包' if value else '战令最终奖励已放入背包'))
|
||||
elif kind=='error':chat.AppendChat(chat.CHAT_TYPE_INFO,L(ERRORS[s.error]))
|
||||
if g_window and g_window.IsShow():g_window.Refresh()
|
||||
|
||||
|
||||
g_pass=Pass(net.SendChatPacket,app.GetTime,OnPass)
|
||||
|
||||
|
||||
def Toggle():
|
||||
if g_window:g_window.Toggle()
|
||||
|
||||
|
||||
_open=game.GameWindow.Open
|
||||
_close=game.GameWindow.Close
|
||||
_command=game.GameWindow.BINARY_ServerCommand_Run
|
||||
|
||||
def Open(self):
|
||||
global g_window,g_entry
|
||||
_open(self);g_pass.waiting=None;g_pass.unsupported=False;g_pass.Clear()
|
||||
g_window=BattlePassWindow(g_pass);g_entry=Entry(self)
|
||||
keys=getattr(self,'onPressKeyDict',None)
|
||||
if keys is not None and app.DIK_F6 not in keys:keys[app.DIK_F6]=lambda:Toggle()
|
||||
|
||||
def Close(self):
|
||||
global g_window,g_entry
|
||||
if g_entry:g_entry.Hide();g_entry.event=None;g_entry=None
|
||||
if g_window:g_window.Destroy();g_window=None
|
||||
_close(self)
|
||||
|
||||
def Command(self,line):
|
||||
if g_pass.Receive(line):return 1
|
||||
return _command(self,line)
|
||||
|
||||
game.GameWindow.Open=Open
|
||||
game.GameWindow.Close=Close
|
||||
game.GameWindow.BINARY_ServerCommand_Run=Command
|
||||
)PY";
|
||||
@@ -0,0 +1,83 @@
|
||||
// PORT: AVA2 costume enchants (ENABLE_SPECIAL_COSTUME_ATTR), as module mt_costume_attr. Items 54001-54267 are
|
||||
// USE_CHANGE_ATTRIBUTE rows with value0 = apply, value1 = value, value2 = target mask (1 << bonus flag); the server
|
||||
// (metin2-server port_costume_attr.cpp) adds that attribute to a body / hair / mount-skin costume. The 40250
|
||||
// inventory already sends every USE_CHANGE_ATTRIBUTE drop to the server; this script only marks the matching
|
||||
// costumes as usable targets, shows the attribute in the enchant's tooltip and renders the "MTCATTR <code>" server
|
||||
// messages that replace AVA2's locale strings 1056 / 1057 / 1061 / 1062 / 1063.
|
||||
static const char c_szCostumeAttrScript[] = R"PY(# -*- coding: utf-8 -*-
|
||||
import chat
|
||||
import game
|
||||
import item
|
||||
import player
|
||||
import uiInventory
|
||||
import uiToolTip
|
||||
|
||||
L=player.AutoHuntLocalize
|
||||
VNUMS=(54001,54267)
|
||||
MAX_ATTR=3
|
||||
# AVA2 CItem::GetBonusFlag of the costume kinds the server maps -> (mask bit, target name).
|
||||
TARGETS={item.COSTUME_TYPE_BODY:(1,'时装'),item.COSTUME_TYPE_HAIR:(2,'发型'),item.COSTUME_TYPE_MOUNT_SKIN:(4,'坐骑外观')}
|
||||
MESSAGES={'target':'该强化石不能用于这件物品。','none':'该强化石没有可附加的属性。','equipped':'请先卸下时装再附加属性。',
|
||||
'full':'这件时装的附加属性已满(最多 3 条)。','has':'这件时装已有该属性。','done':'附加属性成功。'}
|
||||
|
||||
|
||||
def Enchant(vnum):
|
||||
"""(apply, value, mask) of a costume enchant, else None (selects vnum)."""
|
||||
if not VNUMS[0]<=vnum<=VNUMS[1]:return None
|
||||
item.SelectItem(vnum)
|
||||
if item.GetItemType()!=item.ITEM_TYPE_USE or item.GetUseType(vnum)!="USE_CHANGE_ATTRIBUTE" or not item.GetValue(2):return None
|
||||
return (item.GetValue(0),item.GetValue(1),item.GetValue(2))
|
||||
|
||||
|
||||
def CanEnchant(enchant,dstSlot):
|
||||
dstVnum=player.GetItemIndex(dstSlot)
|
||||
if not dstVnum or player.IsEquipmentSlot(dstSlot):return False
|
||||
item.SelectItem(dstVnum)
|
||||
if item.GetItemType()!=item.ITEM_TYPE_COSTUME:return False
|
||||
target=TARGETS.get(item.GetItemSubType())
|
||||
if not target or not enchant[2]&(1<<target[0]):return False
|
||||
attrs=[player.GetItemAttribute(dstSlot,i) for i in xrange(player.ATTRIBUTE_SLOT_MAX_NUM)]
|
||||
used=[a for a in attrs if a[0]]
|
||||
return len(used)<MAX_ATTR and enchant[0] not in [a[0] for a in used]
|
||||
|
||||
|
||||
_canUse=uiInventory.InventoryWindow._InventoryWindow__CanUseSrcItemToDstItem
|
||||
|
||||
def CanUseSrcItemToDstItem(self,srcItemVNum,srcSlotPos,dstSlotPos):
|
||||
enchant=Enchant(srcItemVNum)
|
||||
if enchant is None:return _canUse(self,srcItemVNum,srcSlotPos,dstSlotPos)
|
||||
return srcSlotPos!=dstSlotPos and CanEnchant(enchant,dstSlotPos)
|
||||
|
||||
uiInventory.InventoryWindow._InventoryWindow__CanUseSrcItemToDstItem=CanUseSrcItemToDstItem
|
||||
|
||||
|
||||
Tip=uiToolTip.ItemToolTip
|
||||
_addItemData=Tip.AddItemData
|
||||
|
||||
def AddItemData(self,itemVnum,metinSlot,attrSlot=0,flags=0,unbindTime=0):
|
||||
result=_addItemData(self,itemVnum,metinSlot,attrSlot,flags,unbindTime)
|
||||
enchant=Enchant(itemVnum)
|
||||
if enchant:
|
||||
self.AppendSpace(5)
|
||||
affect=self._ItemToolTip__GetAffectString(enchant[0],enchant[1])
|
||||
if affect:self.AppendTextLine(affect,self.POSITIVE_COLOR)
|
||||
names=[name for bit,name in sorted(TARGETS.values()) if enchant[2]&(1<<bit)]
|
||||
if names:self.AppendTextLine(L('适用:'+'、'.join(names)),self.CONDITION_COLOR)
|
||||
self.ResizeToolTip()
|
||||
return result
|
||||
|
||||
Tip.AddItemData=AddItemData
|
||||
|
||||
|
||||
_command=game.GameWindow.BINARY_ServerCommand_Run
|
||||
|
||||
def Command(self,line):
|
||||
if line.startswith('MTCATTR '):
|
||||
words=line.split()
|
||||
text=MESSAGES.get(words[1] if len(words)>1 else '')
|
||||
if text:chat.AppendChat(chat.CHAT_TYPE_INFO,L(text))
|
||||
return 1
|
||||
return _command(self,line)
|
||||
|
||||
game.GameWindow.BINARY_ServerCommand_Run=Command
|
||||
)PY";
|
||||
@@ -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";
|
||||
@@ -0,0 +1,79 @@
|
||||
// PORT: AVA2 mount costume + mount skin (ENABLE_MOUNT_COSTUME_SYSTEM / ENABLE_MOUNT_SKIN), as module mt_mount.
|
||||
// The server swaps the horse race for the worn costume's value0 (metin2-server port_mount.cpp); this script
|
||||
// registers the AVA2 npclist rows of the skin races (npclist.txt is read from the root pack only, so a patch
|
||||
// pack cannot add them; RaceManager's __IsNewMountRace adds npc_mount to their search path), adds the mount
|
||||
// and skin slots to the costume window under the sash slot, and renders the "MTMOUNT <code>" server
|
||||
// messages that replace AVA2's locale strings 334 / 1097 / 1098. Models ship in the ava2items pack
|
||||
// (tools/server/ava2-items, MOUNT_MOBS).
|
||||
static const char c_szMountScript[] = R"PY(# -*- coding: utf-8 -*-
|
||||
import chat
|
||||
import chrmgr
|
||||
import game
|
||||
import item
|
||||
import player
|
||||
import ui
|
||||
import uiInventory
|
||||
|
||||
L=player.AutoHuntLocalize
|
||||
# AVA2 root/npclist.txt rows of the races MOUNT_MOBS ships.
|
||||
RACES=((32012,"christmas_2017_sleddog_02"),(32028,"christmas_2020_alpaca"))
|
||||
SRC_NAMES=(("christmas_2017_sleddog_02","christmas_2017_sleddog"),)
|
||||
MESSAGES={'riding':'骑乘中无法更换坐骑时装。','need_mount':'需要先装备坐骑时装。','has_skin':'请先卸下坐骑外观。',
|
||||
'need_horse':'需要先拥有马匹(马匹等级 1 以上)才能召唤坐骑。'}
|
||||
# Costume window: left column under the sash slot (CostumeSlot coordinates; body is at 61,45).
|
||||
SLOTS=((item.COSTUME_SLOT_MOUNT,(13,45)),(item.COSTUME_SLOT_MOUNT_SKIN,(13,82)))
|
||||
|
||||
for race,name in RACES:chrmgr.RegisterRaceName(race,name)
|
||||
for name,src in SRC_NAMES:chrmgr.RegisterRaceSrcName(name,src)
|
||||
|
||||
|
||||
def FindChild(node,name):
|
||||
if node.get("name")==name:return node
|
||||
for child in node.get("children",()):
|
||||
found=FindChild(child,name)
|
||||
if found:return found
|
||||
return None
|
||||
|
||||
|
||||
def PatchCostumeWindow(window):
|
||||
base=FindChild(window,"Costume_Base");slot=FindChild(window,"CostumeSlot")
|
||||
if not base or not slot:return
|
||||
frames=[];slots=[]
|
||||
for index,(x,y) in SLOTS:
|
||||
frames.append({"name":"MountSlotBase%d"%index,"type":"image","x":slot["x"]+x,"y":slot["y"]+y,"image":"d:/ymir work/ui/public/Slot_Base.sub"})
|
||||
slots.append({"index":index,"x":x,"y":y,"width":32,"height":32})
|
||||
base["children"]=tuple(frames)+tuple(base.get("children",()))
|
||||
slot["slot"]=tuple(slot["slot"])+tuple(slots)
|
||||
|
||||
|
||||
_execfile=ui.execfile
|
||||
|
||||
def ExecFile(fileName,dict):
|
||||
_execfile(fileName,dict)
|
||||
name=fileName.replace("\\","/").split("/")[-1].lower()
|
||||
if name=="costumewindow.py" and "window" in dict:PatchCostumeWindow(dict["window"])
|
||||
|
||||
ui.execfile=ExecFile
|
||||
|
||||
_refreshCostume=uiInventory.CostumeWindow.RefreshCostumeSlot
|
||||
|
||||
def RefreshCostumeSlot(self):
|
||||
_refreshCostume(self)
|
||||
for index,_ in SLOTS:self.wndEquip.SetItemSlot(index,player.GetItemIndex(index),0)
|
||||
self.wndEquip.RefreshSlot()
|
||||
|
||||
uiInventory.CostumeWindow.RefreshCostumeSlot=RefreshCostumeSlot
|
||||
|
||||
|
||||
_command=game.GameWindow.BINARY_ServerCommand_Run
|
||||
|
||||
def Command(self,line):
|
||||
if line.startswith('MTMOUNT '):
|
||||
words=line.split()
|
||||
text=MESSAGES.get(words[1] if len(words)>1 else '')
|
||||
if text:chat.AppendChat(chat.CHAT_TYPE_INFO,L(text))
|
||||
return 1
|
||||
return _command(self,line)
|
||||
|
||||
game.GameWindow.BINARY_ServerCommand_Run=Command
|
||||
)PY";
|
||||
@@ -0,0 +1,100 @@
|
||||
// PORT: AVA2 talisman (ITEM_TALISMAN_EQUIPMENT + ELEMENT_NEW_BONUSES), as module mt_pendant. Talismans are
|
||||
// ITEM_ARMOR / ARMOR_PENDANT worn in c_Equipment_Pendant (server WEAR_PENDANT, metin2-server port_pendant.cpp).
|
||||
// This script adds that slot to the inventory's equipment panel (the free spot under the earring on
|
||||
// equipment_base), marks unequipped talismans as drop targets of the talisman attribute items (71302 / 53996
|
||||
// reroll, 71301 / 53997 add up to 5) and nothing else, and renders the "MTPENDANT <code>" server messages that
|
||||
// replace AVA2's locale strings 1056 / 1057 / 1067.
|
||||
static const char c_szPendantScript[] = R"PY(# -*- coding: utf-8 -*-
|
||||
import chat
|
||||
import game
|
||||
import item
|
||||
import player
|
||||
import ui
|
||||
import uiInventory
|
||||
|
||||
L=player.AutoHuntLocalize
|
||||
CHANGE=(71302,53996)
|
||||
ADD=(71301,53997)
|
||||
MAX_ATTR=5
|
||||
MESSAGES={'target':'该道具只能用于护符。','equipped':'请先卸下护符再使用。','empty':'这件护符没有可重洗的属性。',
|
||||
'changed':'护符属性已重新随机。','full':'这件护符的属性已满(最多 5 条)。','added':'护符追加属性成功。'}
|
||||
# EquipmentSlot coordinates of the slot (the earring is at 114,84).
|
||||
SLOT=(item.EQUIPMENT_PENDANT,(114,118))
|
||||
|
||||
|
||||
def FindChild(node,name):
|
||||
if node.get("name")==name:return node
|
||||
for child in node.get("children",()):
|
||||
found=FindChild(child,name)
|
||||
if found:return found
|
||||
return None
|
||||
|
||||
|
||||
def PatchInventoryWindow(window):
|
||||
base=FindChild(window,"Equipment_Base");slot=FindChild(window,"EquipmentSlot")
|
||||
if not base or not slot:return
|
||||
index,(x,y)=SLOT
|
||||
frame={"name":"PendantSlotBase","type":"image","x":slot["x"]+x,"y":slot["y"]+y,"image":"d:/ymir work/ui/public/Slot_Base.sub"}
|
||||
base["children"]=(frame,)+tuple(base.get("children",()))
|
||||
slot["slot"]=tuple(slot["slot"])+({"index":index,"x":x,"y":y,"width":32,"height":32},)
|
||||
|
||||
|
||||
_execfile=ui.execfile
|
||||
|
||||
def ExecFile(fileName,dict):
|
||||
_execfile(fileName,dict)
|
||||
name=fileName.replace("\\","/").split("/")[-1].lower()
|
||||
if name=="inventorywindow.py" and "window" in dict:PatchInventoryWindow(dict["window"])
|
||||
|
||||
ui.execfile=ExecFile
|
||||
|
||||
|
||||
_refreshEquip=uiInventory.InventoryWindow.RefreshEquipSlotWindow
|
||||
|
||||
def RefreshEquipSlotWindow(self):
|
||||
_refreshEquip(self)
|
||||
index=SLOT[0]
|
||||
self.wndEquip.SetItemSlot(index,player.GetItemIndex(index),0)
|
||||
self.wndEquip.RefreshSlot()
|
||||
|
||||
uiInventory.InventoryWindow.RefreshEquipSlotWindow=RefreshEquipSlotWindow
|
||||
|
||||
|
||||
def IsTalisman(vnum):
|
||||
if not vnum:return False
|
||||
item.SelectItem(vnum)
|
||||
return item.GetItemType()==item.ITEM_TYPE_ARMOR and item.GetItemSubType()==item.ARMOR_PENDANT
|
||||
|
||||
|
||||
def AttrCount(slot):
|
||||
return len([1 for i in xrange(player.ATTRIBUTE_SLOT_MAX_NUM) if player.GetItemAttribute(slot,i)[0]])
|
||||
|
||||
|
||||
_canUse=uiInventory.InventoryWindow._InventoryWindow__CanUseSrcItemToDstItem
|
||||
|
||||
def CanUseSrcItemToDstItem(self,srcItemVNum,srcSlotPos,dstSlotPos):
|
||||
scroll=srcItemVNum in CHANGE or srcItemVNum in ADD
|
||||
talisman=IsTalisman(player.GetItemIndex(dstSlotPos))
|
||||
if not scroll:
|
||||
# the server refuses every other attribute / socket item on a talisman
|
||||
if talisman and item.GetUseType(srcItemVNum) in self.USE_TYPE_TUPLE:return False
|
||||
return _canUse(self,srcItemVNum,srcSlotPos,dstSlotPos)
|
||||
if srcSlotPos==dstSlotPos or not talisman or player.IsEquipmentSlot(dstSlotPos):return False
|
||||
count=AttrCount(dstSlotPos)
|
||||
return count>0 if srcItemVNum in CHANGE else count<MAX_ATTR
|
||||
|
||||
uiInventory.InventoryWindow._InventoryWindow__CanUseSrcItemToDstItem=CanUseSrcItemToDstItem
|
||||
|
||||
|
||||
_command=game.GameWindow.BINARY_ServerCommand_Run
|
||||
|
||||
def Command(self,line):
|
||||
if line.startswith('MTPENDANT '):
|
||||
words=line.split()
|
||||
text=MESSAGES.get(words[1] if len(words)>1 else '')
|
||||
if text:chat.AppendChat(chat.CHAT_TYPE_INFO,L(text))
|
||||
return 1
|
||||
return _command(self,line)
|
||||
|
||||
game.GameWindow.BINARY_ServerCommand_Run=Command
|
||||
)PY";
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "UserInterface/PythonQuest.h"
|
||||
#include "UserInterface/PythonMessenger.h"
|
||||
#include "UserInterface/PythonSafeBox.h"
|
||||
#include "UserInterface/PythonSash.h"
|
||||
#include "UserInterface/PythonGuild.h"
|
||||
#include "UserInterface/MarkManager.h"
|
||||
#include "UserInterface/GuildMarkDownloader.h"
|
||||
@@ -44,6 +45,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"
|
||||
@@ -152,11 +154,13 @@ struct GameSingletons
|
||||
CPythonQuest pyQuest;
|
||||
CPythonMessenger pyManager;
|
||||
CPythonSafeBox pySafeBox;
|
||||
CPythonSash pySash; // PORT: AVA2 sash
|
||||
CPythonGuild pyGuild;
|
||||
CGuildMarkManager kGuildMarkManager;
|
||||
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;
|
||||
@@ -909,8 +913,10 @@ bool run_main_script_body(const char* lpCmdLine, std::string* error)
|
||||
initBackground();
|
||||
initMessenger();
|
||||
initsafebox();
|
||||
initSash();
|
||||
initguild();
|
||||
initServerStateChecker();
|
||||
initRenderTarget(); // PORT: AVA2 model preview
|
||||
init_2v0_gameplay_stubs();
|
||||
|
||||
ClearLogBoxMessage();
|
||||
@@ -1006,6 +1012,15 @@ namespace PythonBoot
|
||||
#include "ChannelScript.inc"
|
||||
#include "BossScript.inc"
|
||||
#include "VipScript.inc"
|
||||
#include "ModelPreviewScript.inc"
|
||||
#include "BattlePassScript.inc"
|
||||
#include "AppliesScript.inc"
|
||||
#include "ArmorShapesScript.inc"
|
||||
#include "SashScript.inc"
|
||||
#include "MountScript.inc"
|
||||
#include "CostumeAttrScript.inc"
|
||||
#include "PendantScript.inc"
|
||||
#include "RingsScript.inc"
|
||||
|
||||
static void install_script_module(const char* c_szModule, const char* c_szScript)
|
||||
{
|
||||
@@ -1086,6 +1101,15 @@ 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);
|
||||
install_script_module("mt_battlepass", c_szBattlePassScript);
|
||||
install_script_module("mt_applies", c_szAppliesScript);
|
||||
install_script_module("mt_shapes", c_szArmorShapesScript);
|
||||
install_script_module("mt_sash", c_szSashScript);
|
||||
install_script_module("mt_mount", c_szMountScript);
|
||||
install_script_module("mt_costume_attr", c_szCostumeAttrScript);
|
||||
install_script_module("mt_pendant", c_szPendantScript);
|
||||
install_script_module("mt_rings", c_szRingsScript);
|
||||
if (g_touch_input)
|
||||
install_script_module("mt_mobile_hud", c_szMobileHUDScript);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
// PORT: AVA2 new rings (ENABLE_NEW_RINGS), as module mt_rings. Rings are ITEM_TYPE_RINGS (36) worn in
|
||||
// c_Equipment_Rings1/2 (server WEAR_RINGS_1/2, metin2-server port_rings.cpp). They are socket accessories with
|
||||
// their own attribute pool, reached through the 40250 switchers / adders. This script adds the two slots to the
|
||||
// inventory's equipment panel (the free spots beside the shoes on equipment_base, where AVA2 has them too), gives
|
||||
// rings the accessory tooltip of AVA2 uitooltip.py (limits, applies, attributes, jewel sockets) and marks them as
|
||||
// drop targets of the drill / jewel / switcher / adder items the server accepts.
|
||||
static const char c_szRingsScript[] = R"PY(# -*- coding: utf-8 -*-
|
||||
import item
|
||||
import player
|
||||
import uiInventory
|
||||
import uiToolTip
|
||||
import ui
|
||||
|
||||
# EquipmentSlot coordinates (the shoes are at 39,145).
|
||||
SLOTS=((item.EQUIPMENT_RINGS1,(2,145)),(item.EQUIPMENT_RINGS2,(75,145)))
|
||||
# AVA2 CItem::CanPutInto: ring family (vnum / 10 * 10) -> jewel.
|
||||
JEWEL={19220:50634,19500:50635,19520:50636,19540:50637,19560:50638}
|
||||
ACCESSORY_SOCKET_MAX_SIZE=3
|
||||
|
||||
|
||||
def IsRing(vnum):
|
||||
if not vnum:return False
|
||||
item.SelectItem(vnum)
|
||||
return item.GetItemType()==item.ITEM_TYPE_RINGS
|
||||
|
||||
|
||||
def MaterialVnum(vnum):
|
||||
return JEWEL.get(vnum/10*10,0)
|
||||
|
||||
|
||||
def FindChild(node,name):
|
||||
if node.get("name")==name:return node
|
||||
for child in node.get("children",()):
|
||||
found=FindChild(child,name)
|
||||
if found:return found
|
||||
return None
|
||||
|
||||
|
||||
def PatchInventoryWindow(window):
|
||||
base=FindChild(window,"Equipment_Base");slot=FindChild(window,"EquipmentSlot")
|
||||
if not base or not slot:return
|
||||
frames=[];cells=[]
|
||||
for i,(index,(x,y)) in enumerate(SLOTS):
|
||||
frames.append({"name":"RingSlotBase%d"%i,"type":"image","x":slot["x"]+x,"y":slot["y"]+y,"image":"d:/ymir work/ui/public/Slot_Base.sub"})
|
||||
cells.append({"index":index,"x":x,"y":y,"width":32,"height":32})
|
||||
base["children"]=tuple(frames)+tuple(base.get("children",()))
|
||||
slot["slot"]=tuple(slot["slot"])+tuple(cells)
|
||||
|
||||
|
||||
_execfile=ui.execfile
|
||||
|
||||
def ExecFile(fileName,dict):
|
||||
_execfile(fileName,dict)
|
||||
name=fileName.replace("\\","/").split("/")[-1].lower()
|
||||
if name=="inventorywindow.py" and "window" in dict:PatchInventoryWindow(dict["window"])
|
||||
|
||||
ui.execfile=ExecFile
|
||||
|
||||
|
||||
_refreshEquip=uiInventory.InventoryWindow.RefreshEquipSlotWindow
|
||||
|
||||
def RefreshEquipSlotWindow(self):
|
||||
_refreshEquip(self)
|
||||
for index,_ in SLOTS:self.wndEquip.SetItemSlot(index,player.GetItemIndex(index),0)
|
||||
self.wndEquip.RefreshSlot()
|
||||
|
||||
uiInventory.InventoryWindow.RefreshEquipSlotWindow=RefreshEquipSlotWindow
|
||||
|
||||
|
||||
def AttrCount(slot):
|
||||
return len([1 for i in xrange(player.ATTRIBUTE_SLOT_MAX_NUM) if player.GetItemAttribute(slot,i)[0]])
|
||||
|
||||
|
||||
# 40250's __CanPutAccessorySocket / __CanAddAccessorySocket / __CanChangeItemAttrList / __CanAddItemAttr only take
|
||||
# weapons, armor and the armor accessories; everything else (refine scrolls, ...) keeps the 40250 checks.
|
||||
RING_USE_TYPES=("USE_ADD_ACCESSORY_SOCKET","USE_PUT_INTO_ACCESSORY_SOCKET","USE_CHANGE_ATTRIBUTE","USE_ADD_ATTRIBUTE","USE_ADD_ATTRIBUTE2")
|
||||
|
||||
|
||||
def CanUseOnRing(srcItemVNum,dstSlotPos,useType):
|
||||
dstItemVNum=player.GetItemIndex(dstSlotPos)
|
||||
# the server refuses these items on a worn item ("Fix" in CHARACTER::UseItemEx)
|
||||
if player.IsEquipmentSlot(dstSlotPos):return False
|
||||
cur=player.GetItemMetinSocket(dstSlotPos,0);end=player.GetItemMetinSocket(dstSlotPos,1)
|
||||
if useType=="USE_ADD_ACCESSORY_SOCKET":return end<ACCESSORY_SOCKET_MAX_SIZE
|
||||
if useType=="USE_PUT_INTO_ACCESSORY_SOCKET":return srcItemVNum==MaterialVnum(dstItemVNum) and cur<end
|
||||
# 71151 / 76023 / 71152 / 76024 are for weapons / armor
|
||||
if srcItemVNum in (71151,76023,71152,76024):return False
|
||||
count=AttrCount(dstSlotPos)
|
||||
if useType=="USE_CHANGE_ATTRIBUTE":return count>0
|
||||
if useType=="USE_ADD_ATTRIBUTE":return count<4
|
||||
if useType=="USE_ADD_ATTRIBUTE2":return count==4
|
||||
return False
|
||||
|
||||
|
||||
_canUse=uiInventory.InventoryWindow._InventoryWindow__CanUseSrcItemToDstItem
|
||||
|
||||
def CanUseSrcItemToDstItem(self,srcItemVNum,srcSlotPos,dstSlotPos):
|
||||
useType=item.GetUseType(srcItemVNum)
|
||||
if srcSlotPos!=dstSlotPos and useType in RING_USE_TYPES and IsRing(player.GetItemIndex(dstSlotPos)):
|
||||
return CanUseOnRing(srcItemVNum,dstSlotPos,useType)
|
||||
return _canUse(self,srcItemVNum,srcSlotPos,dstSlotPos)
|
||||
|
||||
uiInventory.InventoryWindow._InventoryWindow__CanUseSrcItemToDstItem=CanUseSrcItemToDstItem
|
||||
|
||||
|
||||
# --- tooltip (AVA2 uitooltip.py ITEM_TYPE_RINGS branch, without rarity) ---------------------------------------
|
||||
# A ring falls into the generic branch of ItemToolTip.AddItemData, which only appends the limits; the rest of the
|
||||
# accessory tooltip follows them.
|
||||
Tip=uiToolTip.ItemToolTip
|
||||
_addItemData=Tip.AddItemData
|
||||
_appendLimit=Tip._ItemToolTip__AppendLimitInformation
|
||||
|
||||
def AddItemData(self,itemVnum,metinSlot,attrSlot=0,flags=0,unbindTime=0):
|
||||
self.mtRing=(itemVnum,metinSlot,attrSlot) if IsRing(itemVnum) else None
|
||||
try:
|
||||
return _addItemData(self,itemVnum,metinSlot,attrSlot,flags,unbindTime)
|
||||
finally:
|
||||
self.mtRing=None
|
||||
|
||||
|
||||
def AppendLimitInformation(self):
|
||||
_appendLimit(self)
|
||||
state=getattr(self,'mtRing',None)
|
||||
if not state:return
|
||||
self.mtRing=None
|
||||
itemVnum,metinSlot,attrSlot=state
|
||||
item.SelectItem(itemVnum)
|
||||
self._ItemToolTip__AppendAffectInformation()
|
||||
self._ItemToolTip__AppendAttributeInformation(attrSlot)
|
||||
item.SelectItem(itemVnum)
|
||||
self.AppendWearableInformation()
|
||||
if metinSlot:
|
||||
item.SelectItem(itemVnum)
|
||||
self._ItemToolTip__AppendAccessoryMetinSlotInfo(metinSlot,MaterialVnum(itemVnum))
|
||||
item.SelectItem(itemVnum)
|
||||
|
||||
Tip.AddItemData=AddItemData
|
||||
Tip._ItemToolTip__AppendLimitInformation=AppendLimitInformation
|
||||
)PY";
|
||||
@@ -0,0 +1,612 @@
|
||||
// PORT: AVA2 sash (披风) windows and tooltips, as module mt_sash. The C++ side is the AVA2 sash module
|
||||
// (PythonSash.cpp, CG/GC_SASH packets, PART_SASH model + item_scale.txt); this script is AVA2 root/uisash.py
|
||||
// (CombineWindow/AbsorbWindow verbatim, their uiscript dicts served through ui.execfile), the glue AVA2 put
|
||||
// into game.py/interfacemodule.py/uiinventory.py (ActSash/AlertSash, right click adds to the open window,
|
||||
// attached slots lit, costume window sash slot) and uitooltip.py's absorption lines (CalcSashValue,
|
||||
// SetSashResultItem/SetSashResultAbsItem). Server messages arrive as "MTSASH <code>" commands
|
||||
// (metin2-server port_sash.cpp). Assets ship in the ava2items pack (tools/server/ava2-items, SASH_*).
|
||||
static const char c_szSashScript[] = R"PY(# -*- coding: utf-8 -*-
|
||||
import app
|
||||
import chat
|
||||
import chrmgr
|
||||
import constInfo
|
||||
import game
|
||||
import item
|
||||
import localeInfo
|
||||
import mouseModule
|
||||
import player
|
||||
import sash
|
||||
import ui
|
||||
import uiInventory
|
||||
import uiToolTip
|
||||
|
||||
L=player.AutoHuntLocalize
|
||||
|
||||
MESSAGES={'already_open':'披风窗口已经打开','absorb_open':'请先关闭披风吸收窗口','combine_open':'请先关闭披风合成窗口',
|
||||
'locked':'物品已锁定,无法放入','grade':'只能放入相同品级的披风','max_abs':'该披风的吸收率已达上限',
|
||||
'absorb_type':'只能吸收武器或铠甲','absorbed':'该披风已经吸收过物品','gold':'金币不足',
|
||||
'success':'成功!','fail':'失败,作为材料的披风已消失'}
|
||||
ABS_CHANGED='合成完成,吸收率变为 %d%%'
|
||||
ALERT_COMBINE='合成失败时,作为材料的第二件披风将会消失。'
|
||||
ALERT_ABSORB='吸收后,被吸收的物品将会消失。'
|
||||
ABSORB_CHANCE='吸收率:%d%%'
|
||||
ABSORB_CHANCE2='吸收率:%d%% ~ %d%%'
|
||||
|
||||
COMBINE_SCRIPT='''# -*- coding: utf-8 -*-
|
||||
import player
|
||||
import uiScriptLocale
|
||||
L=player.AutoHuntLocalize
|
||||
window = {
|
||||
"name" : "Sash_CombineWindow",
|
||||
"x" : (SCREEN_WIDTH - 215)/2, "y" : (SCREEN_HEIGHT - 270)/2,
|
||||
"style" : ("movable", "float",),
|
||||
"width" : 215, "height" : 270,
|
||||
"children" : (
|
||||
{
|
||||
"name" : "board", "type" : "board", "style" : ("attach",),
|
||||
"x" : 0, "y" : 0, "width" : 215, "height" : 270,
|
||||
"children" : (
|
||||
{
|
||||
"name" : "TitleBar", "type" : "titlebar", "style" : ("attach",),
|
||||
"x" : 6, "y" : 6, "width" : 200, "color" : "yellow",
|
||||
"children" : (
|
||||
{ "name" : "TitleName", "type" : "text", "x" : 95, "y" : 3, "text" : L("披风合成"), "text_horizontal_align" : "center" },
|
||||
),
|
||||
},
|
||||
{
|
||||
"name" : "Sash_Combine", "type" : "image", "x" : 9, "y" : 35,
|
||||
"image" : uiScriptLocale.LOCALE_UISCRIPT_PATH + "acce/acce_combine.tga",
|
||||
"children" : (
|
||||
{
|
||||
"name" : "SashSlot", "type" : "slot", "x" : 3, "y" : 3, "width" : 200, "height" : 150,
|
||||
"slot" : (
|
||||
{"index":0, "x":78, "y":7, "width":32, "height":32},
|
||||
{"index":1, "x":78, "y":60, "width":32, "height":32},
|
||||
{"index":2, "x":78, "y":115, "width":32, "height":32},
|
||||
),
|
||||
},
|
||||
{ "name" : "Main", "type" : "text", "text" : L("主披风"), "text_horizontal_align" : "center", "x" : 85 + 12, "y" : 7 + 36 },
|
||||
{ "name" : "serve", "type" : "text", "text" : L("材料披风"), "text_horizontal_align" : "center", "x" : 85 + 12, "y" : 60 + 38 },
|
||||
{ "name" : "Result", "type" : "text", "text" : L("合成结果"), "text_horizontal_align" : "center", "x" : 85 + 12, "y" : 115 + 40 },
|
||||
),
|
||||
},
|
||||
{ "name" : "NeedMoney", "type" : "text", "text" : "", "text_horizontal_align" : "center", "x" : 105, "y" : 215 },
|
||||
{
|
||||
"name" : "AcceptButton", "type" : "button", "x" : 40, "y" : 235, "text" : uiScriptLocale.OK,
|
||||
"default_image" : "d:/ymir work/ui/public/middle_button_01.sub",
|
||||
"over_image" : "d:/ymir work/ui/public/middle_button_02.sub",
|
||||
"down_image" : "d:/ymir work/ui/public/middle_button_03.sub",
|
||||
},
|
||||
{
|
||||
"name" : "CancelButton", "type" : "button", "x" : 114, "y" : 235, "text" : uiScriptLocale.CANCEL,
|
||||
"default_image" : "d:/ymir work/ui/public/middle_button_01.sub",
|
||||
"over_image" : "d:/ymir work/ui/public/middle_button_02.sub",
|
||||
"down_image" : "d:/ymir work/ui/public/middle_button_03.sub",
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
}
|
||||
'''
|
||||
|
||||
ABSORB_SCRIPT='''# -*- coding: utf-8 -*-
|
||||
import player
|
||||
import uiScriptLocale
|
||||
L=player.AutoHuntLocalize
|
||||
window = {
|
||||
"name" : "Sash_AbsorbtionWindow",
|
||||
"x" : (SCREEN_WIDTH - 205)/2, "y" : (SCREEN_HEIGHT - 270)/2,
|
||||
"style" : ("movable", "float",),
|
||||
"width" : 205, "height" : 270,
|
||||
"children" : (
|
||||
{
|
||||
"name" : "board", "type" : "board", "style" : ("attach",),
|
||||
"x" : 0, "y" : 0, "width" : 205, "height" : 270,
|
||||
"children" : (
|
||||
{
|
||||
"name" : "TitleBar", "type" : "titlebar", "style" : ("attach",),
|
||||
"x" : 6, "y" : 6, "width" : 190, "color" : "yellow",
|
||||
"children" : (
|
||||
{ "name" : "TitleName", "type" : "text", "x" : 95, "y" : 3, "text" : L("披风吸收"), "text_horizontal_align" : "center" },
|
||||
),
|
||||
},
|
||||
{
|
||||
"name" : "Sash_Combine", "type" : "image", "x" : 9, "y" : 35,
|
||||
"image" : uiScriptLocale.LOCALE_UISCRIPT_PATH + "acce/acce_absorb.tga",
|
||||
"children" : (
|
||||
{
|
||||
"name" : "SashSlot", "type" : "slot", "x" : 3, "y" : 3, "width" : 190, "height" : 200,
|
||||
"slot" : (
|
||||
{"index":0, "x":26, "y":41, "width":31, "height":31},
|
||||
{"index":1, "x":125, "y":8, "width":31, "height":96},
|
||||
{"index":2, "x":75, "y":126, "width":31, "height":31},
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
"name" : "AcceptButton", "type" : "button", "x" : 40, "y" : 235, "text" : uiScriptLocale.OK,
|
||||
"default_image" : "d:/ymir work/ui/public/middle_button_01.sub",
|
||||
"over_image" : "d:/ymir work/ui/public/middle_button_02.sub",
|
||||
"down_image" : "d:/ymir work/ui/public/middle_button_03.sub",
|
||||
},
|
||||
{
|
||||
"name" : "CancelButton", "type" : "button", "x" : 114, "y" : 235, "text" : uiScriptLocale.CANCEL,
|
||||
"default_image" : "d:/ymir work/ui/public/middle_button_01.sub",
|
||||
"over_image" : "d:/ymir work/ui/public/middle_button_02.sub",
|
||||
"down_image" : "d:/ymir work/ui/public/middle_button_03.sub",
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
}
|
||||
'''
|
||||
|
||||
SCRIPTS={'sash_combinewindow.py':COMBINE_SCRIPT,'sash_absorbwindow.py':ABSORB_SCRIPT}
|
||||
# Costume window: the sash slot left of the hair slot (CostumeSlot coordinates).
|
||||
COSTUME_SASH_POS=(13,8)
|
||||
|
||||
g_windows=None
|
||||
g_effects=False
|
||||
|
||||
|
||||
def CalcSashValue(value,abs):
|
||||
# AVA2 uitooltip.CalcSashValue; the server scales applies the same way (port_sash.cpp).
|
||||
if not value:return 0
|
||||
valueCalc=round(value*abs)/100
|
||||
valueCalc+=0.5
|
||||
valueCalc=int(valueCalc)+1 if valueCalc>0 else int(valueCalc)
|
||||
return 1 if (valueCalc<=0 and value>0) else valueCalc
|
||||
|
||||
|
||||
def IsSash(itemVnum):
|
||||
item.SelectItem(itemVnum)
|
||||
return item.GetItemType()==item.ITEM_TYPE_COSTUME and item.GetItemSubType()==item.COSTUME_TYPE_SASH
|
||||
|
||||
|
||||
def IsSashWindowOpen():
|
||||
return bool(g_windows) and (g_windows[0].IsOpened() or g_windows[1].IsOpened())
|
||||
|
||||
|
||||
class CombineWindow(ui.ScriptWindow):
|
||||
def __init__(self):
|
||||
ui.ScriptWindow.__init__(self)
|
||||
self.isLoaded=0;self.PositionOut=0;self.PositionStartX=0;self.PositionStartY=0
|
||||
self.tooltipItem=None
|
||||
|
||||
def __del__(self):
|
||||
ui.ScriptWindow.__del__(self)
|
||||
|
||||
def Destroy(self):
|
||||
self.ClearDictionary()
|
||||
self.titleBar=None;self.btnAccept=None;self.btnCancel=None;self.sashSlot=None;self.needMoney=None;self.Result=None
|
||||
self.tooltipItem=None;self.PositionOut=0;self.PositionStartX=0;self.PositionStartY=0
|
||||
|
||||
def LoadWindow(self):
|
||||
if self.isLoaded:return
|
||||
self.isLoaded=1
|
||||
try:
|
||||
pyScrLoader=ui.PythonScriptLoader()
|
||||
pyScrLoader.LoadScriptFile(self,"uiscript/sash_combinewindow.py")
|
||||
except:
|
||||
import exception
|
||||
exception.Abort("Sash_CombineWindow.LoadDialog.LoadScript")
|
||||
try:
|
||||
self.board=self.GetChild("board")
|
||||
self.titleBar=self.GetChild("TitleBar")
|
||||
self.btnAccept=self.GetChild("AcceptButton")
|
||||
self.btnCancel=self.GetChild("CancelButton")
|
||||
self.needMoney=self.GetChild("NeedMoney")
|
||||
self.Result=self.GetChild("Result")
|
||||
self.sashSlot=self.GetChild("SashSlot")
|
||||
except:
|
||||
import exception
|
||||
exception.Abort("Sash_CombineWindow.LoadDialog.BindObject")
|
||||
self.sashSlot.SetSelectEmptySlotEvent(ui.__mem_func__(self.OnSelectEmptySlot))
|
||||
self.sashSlot.SetUnselectItemSlotEvent(ui.__mem_func__(self.OnSelectItemSlot))
|
||||
self.sashSlot.SetUseSlotEvent(ui.__mem_func__(self.OnSelectItemSlot))
|
||||
self.sashSlot.SetOverInItemEvent(ui.__mem_func__(self.OnOverInItem))
|
||||
self.sashSlot.SetOverOutItemEvent(ui.__mem_func__(self.OnOverOutItem))
|
||||
self.titleBar.SetCloseEvent(ui.__mem_func__(self.OnClose))
|
||||
self.btnCancel.SetEvent(ui.__mem_func__(self.OnClose))
|
||||
self.btnAccept.SetEvent(ui.__mem_func__(self.OnAccept))
|
||||
|
||||
def SetItemToolTip(self,itemTooltip):
|
||||
self.tooltipItem=itemTooltip
|
||||
|
||||
def IsOpened(self):
|
||||
return bool(self.IsShow() and self.isLoaded)
|
||||
|
||||
def SetPrice(self):
|
||||
self.needMoney.SetText(L('合成费用:')+localeInfo.NumberToMoneyString(sash.GetPrice()))
|
||||
|
||||
def Open(self):
|
||||
self.PositionOut=0
|
||||
(self.PositionStartX,self.PositionStartY,z)=player.GetMainCharacterPosition()
|
||||
self.SetPrice()
|
||||
for i in xrange(sash.WINDOW_MAX_MATERIALS+1):
|
||||
self.sashSlot.ClearSlot(i)
|
||||
self.Show()
|
||||
|
||||
def Close(self):
|
||||
if self.tooltipItem:self.tooltipItem.HideToolTip()
|
||||
self.Hide()
|
||||
|
||||
def OnClose(self):
|
||||
sash.SendCloseRequest()
|
||||
|
||||
def OnPressEscapeKey(self):
|
||||
self.OnClose()
|
||||
return True
|
||||
|
||||
def OnAccept(self):
|
||||
sash.SendRefineRequest()
|
||||
|
||||
def OnUpdate(self):
|
||||
LIMIT_RANGE=sash.LIMIT_RANGE
|
||||
(x,y,z)=player.GetMainCharacterPosition()
|
||||
if abs(x-self.PositionStartX)>=LIMIT_RANGE or abs(y-self.PositionStartY)>=LIMIT_RANGE:
|
||||
if not self.PositionOut:
|
||||
self.PositionOut+=1
|
||||
self.OnClose()
|
||||
|
||||
def OnSelectEmptySlot(self,selectedSlotPos):
|
||||
isAttached=mouseModule.mouseController.isAttached()
|
||||
if not isAttached or selectedSlotPos==sash.WINDOW_MAX_MATERIALS:return
|
||||
attachedSlotType=mouseModule.mouseController.GetAttachedType()
|
||||
attachedSlotPos=mouseModule.mouseController.GetAttachedSlotNumber()
|
||||
attachedInvenType=player.SlotTypeToInvenType(attachedSlotType)
|
||||
mouseModule.mouseController.DeattachObject()
|
||||
if attachedSlotType==player.SLOT_TYPE_INVENTORY and attachedInvenType==player.INVENTORY:
|
||||
sash.Add(attachedInvenType,attachedSlotPos,selectedSlotPos)
|
||||
|
||||
def OnSelectItemSlot(self,selectedSlotPos):
|
||||
if selectedSlotPos==sash.WINDOW_MAX_MATERIALS:return
|
||||
mouseModule.mouseController.DeattachObject()
|
||||
sash.Remove(selectedSlotPos)
|
||||
|
||||
def OnOverInItem(self,selectedSlotPos):
|
||||
if self.tooltipItem:
|
||||
if selectedSlotPos==sash.WINDOW_MAX_MATERIALS:
|
||||
(isHere,iCell)=sash.GetAttachedItem(0)
|
||||
if isHere:self.tooltipItem.SetSashResultItem(iCell)
|
||||
else:
|
||||
(isHere,iCell)=sash.GetAttachedItem(selectedSlotPos)
|
||||
if isHere:self.tooltipItem.SetInventoryItem(iCell)
|
||||
|
||||
def OnOverOutItem(self):
|
||||
if self.tooltipItem:self.tooltipItem.HideToolTip()
|
||||
|
||||
def Refresh(self,iAct):
|
||||
self.SetPrice()
|
||||
self.sashSlot.ClearSlot(sash.WINDOW_MAX_MATERIALS)
|
||||
for i in xrange(sash.WINDOW_MAX_MATERIALS):
|
||||
self.sashSlot.ClearSlot(i)
|
||||
(isHere,iCell)=sash.GetAttachedItem(i)
|
||||
if isHere:
|
||||
self.sashSlot.SetItemSlot(i,player.GetItemIndex(iCell),0)
|
||||
if i==int(sash.WINDOW_MAX_MATERIALS-1):
|
||||
(itemVnum,MinAbs,MaxAbs)=sash.GetResultItem()
|
||||
if not itemVnum:break
|
||||
self.sashSlot.SetItemSlot(i+1,itemVnum,0)
|
||||
break
|
||||
self.sashSlot.RefreshSlot()
|
||||
|
||||
|
||||
class AbsorbWindow(CombineWindow):
|
||||
# AVA2 AbsorbWindow: CombineWindow without the price line, a two-item result tooltip.
|
||||
def LoadWindow(self):
|
||||
if self.isLoaded:return
|
||||
self.isLoaded=1
|
||||
try:
|
||||
pyScrLoader=ui.PythonScriptLoader()
|
||||
pyScrLoader.LoadScriptFile(self,"uiscript/sash_absorbwindow.py")
|
||||
except:
|
||||
import exception
|
||||
exception.Abort("Sash_AbsorbtionWindow.LoadDialog.LoadScript")
|
||||
try:
|
||||
self.titleBar=self.GetChild("TitleBar")
|
||||
self.btnAccept=self.GetChild("AcceptButton")
|
||||
self.btnCancel=self.GetChild("CancelButton")
|
||||
self.sashSlot=self.GetChild("SashSlot")
|
||||
except:
|
||||
import exception
|
||||
exception.Abort("Sash_AbsorbtionWindow.LoadDialog.BindObject")
|
||||
self.sashSlot.SetSelectEmptySlotEvent(ui.__mem_func__(self.OnSelectEmptySlot))
|
||||
self.sashSlot.SetUnselectItemSlotEvent(ui.__mem_func__(self.OnSelectItemSlot))
|
||||
self.sashSlot.SetUseSlotEvent(ui.__mem_func__(self.OnSelectItemSlot))
|
||||
self.sashSlot.SetOverInItemEvent(ui.__mem_func__(self.OnOverInItem))
|
||||
self.sashSlot.SetOverOutItemEvent(ui.__mem_func__(self.OnOverOutItem))
|
||||
self.titleBar.SetCloseEvent(ui.__mem_func__(self.OnClose))
|
||||
self.btnCancel.SetEvent(ui.__mem_func__(self.OnClose))
|
||||
self.btnAccept.SetEvent(ui.__mem_func__(self.OnAccept))
|
||||
|
||||
def SetPrice(self):
|
||||
pass
|
||||
|
||||
def OnOverInItem(self,selectedSlotPos):
|
||||
if self.tooltipItem:
|
||||
if selectedSlotPos==sash.WINDOW_MAX_MATERIALS:
|
||||
(isHere1,iCell1)=sash.GetAttachedItem(0)
|
||||
(isHere2,iCell2)=sash.GetAttachedItem(1)
|
||||
if isHere1 and isHere2:self.tooltipItem.SetSashResultAbsItem(iCell1,iCell2)
|
||||
else:
|
||||
(isHere,iCell)=sash.GetAttachedItem(selectedSlotPos)
|
||||
if isHere:self.tooltipItem.SetInventoryItem(iCell)
|
||||
|
||||
|
||||
# --- tooltip (AVA2 uitooltip.py ENABLE_SASH_SYSTEM parts) -------------------------------------------------
|
||||
Tip=uiToolTip.ItemToolTip
|
||||
_addItemData=Tip.AddItemData
|
||||
_appendAffect=Tip._ItemToolTip__AppendAffectInformation
|
||||
_appendAttribute=Tip._ItemToolTip__AppendAttributeInformation
|
||||
|
||||
|
||||
def AppendAbsorbedItem(self,itemAbsorbedVnum,absChance):
|
||||
# Attack/defence and applies of the absorbed item, scaled by the absorption rate.
|
||||
item.SelectItem(itemAbsorbedVnum)
|
||||
itemType=item.GetItemType()
|
||||
if itemType==item.ITEM_TYPE_WEAPON:
|
||||
attack=[item.GetValue(3),item.GetValue(4),item.GetValue(5)]
|
||||
magic=[item.GetValue(1),item.GetValue(2),item.GetValue(5)]
|
||||
lines=[]
|
||||
minPower,maxPower,addPower=[CalcSashValue(v,absChance) for v in attack]
|
||||
if maxPower>minPower:lines.append(localeInfo.TOOLTIP_ITEM_ATT_POWER%(minPower+addPower,maxPower+addPower))
|
||||
else:lines.append(localeInfo.TOOLTIP_ITEM_ATT_POWER_ONE_ARG%(minPower+addPower))
|
||||
minMagic,maxMagic,addPower=[CalcSashValue(v,absChance) for v in magic]
|
||||
if magic[0]>0 or magic[1]>0:
|
||||
if maxMagic>minMagic:magicLine=localeInfo.TOOLTIP_ITEM_MAGIC_ATT_POWER%(minMagic+addPower,maxMagic+addPower)
|
||||
else:magicLine=localeInfo.TOOLTIP_ITEM_MAGIC_ATT_POWER_ONE_ARG%(minMagic+addPower)
|
||||
if item.GetItemSubType()==item.WEAPON_FAN:lines.insert(0,magicLine)
|
||||
else:lines.append(magicLine)
|
||||
for line in lines:self.AppendTextLine(line,self.POSITIVE_COLOR)
|
||||
elif itemType==item.ITEM_TYPE_ARMOR:
|
||||
defGrade=CalcSashValue(item.GetValue(1),absChance)
|
||||
defBonus=CalcSashValue(item.GetValue(5)*2,absChance)
|
||||
if defGrade>0:
|
||||
self.AppendSpace(5)
|
||||
self.AppendTextLine(localeInfo.TOOLTIP_ITEM_DEF_GRADE%(defGrade+defBonus),self.GetChangeTextLineColor(defGrade))
|
||||
magicDefencePower=CalcSashValue(item.GetValue(0),absChance)
|
||||
if magicDefencePower>0:
|
||||
self.AppendTextLine(localeInfo.TOOLTIP_ITEM_MAGIC_DEF_POWER%magicDefencePower,self.GetChangeTextLineColor(magicDefencePower))
|
||||
for i in xrange(item.ITEM_APPLY_MAX_NUM):
|
||||
item.SelectItem(itemAbsorbedVnum)
|
||||
(affectType,affectValue)=item.GetAffect(i)
|
||||
affectValue=CalcSashValue(affectValue,absChance)
|
||||
affectString=self._ItemToolTip__GetAffectString(affectType,affectValue)
|
||||
if affectString and affectValue>0:
|
||||
self.AppendTextLine(affectString,self.GetChangeTextLineColor(affectValue))
|
||||
|
||||
|
||||
def AppendScaledAttributes(self,attrSlot,absChance):
|
||||
if not attrSlot:return
|
||||
for i in xrange(player.ATTRIBUTE_SLOT_MAX_NUM):
|
||||
type,value=attrSlot[i][0],attrSlot[i][1]
|
||||
if not value:continue
|
||||
value=CalcSashValue(value,absChance)
|
||||
affectString=self._ItemToolTip__GetAffectString(type,value)
|
||||
if affectString and value>0:
|
||||
self.AppendTextLine(affectString,self._ItemToolTip__GetAttributeColor(i,value))
|
||||
|
||||
|
||||
def AddItemData(self,itemVnum,metinSlot,attrSlot=0,flags=0,unbindTime=0):
|
||||
self.mtSash=None
|
||||
if metinSlot and IsSash(itemVnum):
|
||||
self.mtSash=(itemVnum,int(metinSlot[sash.ABSORPTION_SOCKET]),int(metinSlot[sash.ABSORBED_SOCKET]))
|
||||
try:
|
||||
return _addItemData(self,itemVnum,metinSlot,attrSlot,flags,unbindTime)
|
||||
finally:
|
||||
self.mtSash=None
|
||||
|
||||
|
||||
def AppendAffectInformation(self):
|
||||
_appendAffect(self)
|
||||
state=getattr(self,'mtSash',None)
|
||||
if not state:return
|
||||
itemVnum,absChance,itemAbsorbedVnum=state
|
||||
self.AppendTextLine(L(ABSORB_CHANCE%absChance),self.CONDITION_COLOR)
|
||||
if itemAbsorbedVnum:AppendAbsorbedItem(self,itemAbsorbedVnum,absChance)
|
||||
item.SelectItem(itemVnum)
|
||||
|
||||
|
||||
def AppendAttributeInformation(self,attrSlot):
|
||||
state=getattr(self,'mtSash',None)
|
||||
if not state:return _appendAttribute(self,attrSlot)
|
||||
AppendScaledAttributes(self,attrSlot,state[1])
|
||||
item.SelectItem(state[0])
|
||||
|
||||
|
||||
def SetSashResultItem(self,slotIndex,window_type=player.INVENTORY):
|
||||
(itemVnum,MinAbs,MaxAbs)=sash.GetResultItem()
|
||||
if not itemVnum:return
|
||||
self.ClearToolTip()
|
||||
metinSlot=[player.GetItemMetinSocket(window_type,slotIndex,i) for i in xrange(player.METIN_SOCKET_MAX_NUM)]
|
||||
attrSlot=[player.GetItemAttribute(window_type,slotIndex,i) for i in xrange(player.ATTRIBUTE_SLOT_MAX_NUM)]
|
||||
if not IsSash(itemVnum):return
|
||||
itemDesc=item.GetItemDescription()
|
||||
self._ItemToolTip__AdjustMaxWidth(attrSlot,itemDesc)
|
||||
self._ItemToolTip__SetItemTitle(itemVnum,metinSlot,attrSlot)
|
||||
self.AppendDescription(itemDesc,26)
|
||||
self.AppendDescription(item.GetItemSummary(),26,self.CONDITION_COLOR)
|
||||
self._ItemToolTip__AppendLimitInformation()
|
||||
if MinAbs==MaxAbs:self.AppendTextLine(L(ABSORB_CHANCE%MinAbs),self.CONDITION_COLOR)
|
||||
else:self.AppendTextLine(L(ABSORB_CHANCE2%(MinAbs,MaxAbs)),self.CONDITION_COLOR)
|
||||
itemAbsorbedVnum=int(metinSlot[sash.ABSORBED_SOCKET])
|
||||
if itemAbsorbedVnum:AppendAbsorbedItem(self,itemAbsorbedVnum,MaxAbs)
|
||||
AppendScaledAttributes(self,attrSlot,MaxAbs)
|
||||
item.SelectItem(itemVnum)
|
||||
self.AppendWearableInformation()
|
||||
self.ShowToolTip()
|
||||
|
||||
|
||||
def SetSashResultAbsItem(self,slotIndex1,slotIndex2,window_type=player.INVENTORY):
|
||||
itemVnumSash=player.GetItemIndex(window_type,slotIndex1)
|
||||
itemVnumTarget=player.GetItemIndex(window_type,slotIndex2)
|
||||
if not itemVnumSash or not itemVnumTarget:return
|
||||
self.ClearToolTip()
|
||||
if not IsSash(itemVnumSash):return
|
||||
metinSlot=[player.GetItemMetinSocket(window_type,slotIndex1,i) for i in xrange(player.METIN_SOCKET_MAX_NUM)]
|
||||
attrSlot=[player.GetItemAttribute(window_type,slotIndex2,i) for i in xrange(player.ATTRIBUTE_SLOT_MAX_NUM)]
|
||||
absChance=int(metinSlot[sash.ABSORPTION_SOCKET])
|
||||
itemDesc=item.GetItemDescription()
|
||||
self._ItemToolTip__AdjustMaxWidth(attrSlot,itemDesc)
|
||||
self._ItemToolTip__SetItemTitle(itemVnumSash,metinSlot,attrSlot)
|
||||
self.AppendDescription(itemDesc,26)
|
||||
self.AppendDescription(item.GetItemSummary(),26,self.CONDITION_COLOR)
|
||||
item.SelectItem(itemVnumSash)
|
||||
self._ItemToolTip__AppendLimitInformation()
|
||||
self.AppendTextLine(L(ABSORB_CHANCE%absChance),self.CONDITION_COLOR)
|
||||
AppendAbsorbedItem(self,itemVnumTarget,absChance)
|
||||
AppendScaledAttributes(self,attrSlot,absChance)
|
||||
item.SelectItem(itemVnumSash)
|
||||
self.AppendWearableInformation()
|
||||
self.ShowToolTip()
|
||||
|
||||
|
||||
Tip.AddItemData=AddItemData
|
||||
Tip._ItemToolTip__AppendAffectInformation=AppendAffectInformation
|
||||
Tip._ItemToolTip__AppendAttributeInformation=AppendAttributeInformation
|
||||
Tip.SetSashResultItem=SetSashResultItem
|
||||
Tip.SetSashResultAbsItem=SetSashResultAbsItem
|
||||
|
||||
|
||||
# --- uiscripts: the two sash windows and the costume window's sash slot ------------------------------------
|
||||
def FindChild(node,name):
|
||||
if node.get("name")==name:return node
|
||||
for child in node.get("children",()):
|
||||
found=FindChild(child,name)
|
||||
if found:return found
|
||||
return None
|
||||
|
||||
|
||||
def PatchCostumeWindow(window):
|
||||
base=FindChild(window,"Costume_Base");slot=FindChild(window,"CostumeSlot")
|
||||
if not base or not slot:return
|
||||
x,y=COSTUME_SASH_POS
|
||||
frame={"name":"SashSlotBase","type":"image","x":slot["x"]+x,"y":slot["y"]+y,"image":"d:/ymir work/ui/public/Slot_Base.sub"}
|
||||
base["children"]=(frame,)+tuple(base.get("children",()))
|
||||
slot["slot"]=tuple(slot["slot"])+({"index":item.COSTUME_SLOT_SASH,"x":x,"y":y,"width":32,"height":32},)
|
||||
|
||||
|
||||
_execfile=ui.execfile
|
||||
|
||||
def ExecFile(fileName,dict):
|
||||
name=fileName.replace("\\","/").split("/")[-1].lower()
|
||||
if name in SCRIPTS:
|
||||
exec(SCRIPTS[name],dict)
|
||||
return
|
||||
_execfile(fileName,dict)
|
||||
if name=="costumewindow.py" and "window" in dict:PatchCostumeWindow(dict["window"])
|
||||
|
||||
ui.execfile=ExecFile
|
||||
|
||||
_refreshCostume=uiInventory.CostumeWindow.RefreshCostumeSlot
|
||||
|
||||
def RefreshCostumeSlot(self):
|
||||
_refreshCostume(self)
|
||||
self.wndEquip.SetItemSlot(item.COSTUME_SLOT_SASH,player.GetItemIndex(item.COSTUME_SLOT_SASH),0)
|
||||
self.wndEquip.RefreshSlot()
|
||||
|
||||
uiInventory.CostumeWindow.RefreshCostumeSlot=RefreshCostumeSlot
|
||||
|
||||
|
||||
# --- inventory: right click adds to the open sash window, attached items are lit ----------------------------
|
||||
_useItemSlot=uiInventory.InventoryWindow.UseItemSlot
|
||||
_refreshBag=uiInventory.InventoryWindow.RefreshBagSlotWindow
|
||||
|
||||
def UseItemSlot(self,slotIndex):
|
||||
if IsSashWindowOpen() and app.GetCursor()!=app.SELL:
|
||||
cell=self._InventoryWindow__InventoryLocalSlotPosToGlobalSlotPos(slotIndex)
|
||||
if not player.IsEquipmentSlot(cell):
|
||||
sash.Add(player.INVENTORY,cell,255)
|
||||
return
|
||||
return _useItemSlot(self,slotIndex)
|
||||
|
||||
|
||||
def AutoPotionActive(cell):
|
||||
return constInfo.IS_AUTO_POTION(player.GetItemIndex(cell)) and player.GetItemMetinSocket(cell,0)!=0
|
||||
|
||||
|
||||
def RefreshBagSlotWindow(self):
|
||||
_refreshBag(self)
|
||||
attached=set()
|
||||
if IsSashWindowOpen():
|
||||
for j in xrange(sash.WINDOW_MAX_MATERIALS):
|
||||
(isHere,iCell)=sash.GetAttachedItem(j)
|
||||
if isHere:attached.add(iCell)
|
||||
lit=getattr(self,'mtSashLit',set())
|
||||
now=set()
|
||||
for i in xrange(player.INVENTORY_PAGE_SIZE):
|
||||
cell=self._InventoryWindow__InventoryLocalSlotPosToGlobalSlotPos(i)
|
||||
if cell in attached:
|
||||
self.wndItem.ActivateSlot(i);now.add(i)
|
||||
elif i in lit and not AutoPotionActive(cell):
|
||||
self.wndItem.DeactivateSlot(i)
|
||||
self.mtSashLit=now
|
||||
self.wndItem.RefreshSlot()
|
||||
|
||||
uiInventory.InventoryWindow.UseItemSlot=UseItemSlot
|
||||
uiInventory.InventoryWindow.RefreshBagSlotWindow=RefreshBagSlotWindow
|
||||
|
||||
|
||||
# --- game window: ActSash/AlertSash (called by the GC_SASH packet), MTSASH messages, effects -----------------
|
||||
def RegisterEffects():
|
||||
global g_effects
|
||||
if g_effects:return
|
||||
g_effects=True
|
||||
chrmgr.RegisterEffect(chrmgr.EFFECT_SASH_SUCCEDED,"","d:/ymir work/effect/etc/buff/buff_item6.mse")
|
||||
chrmgr.RegisterEffect(chrmgr.EFFECT_SASH_EQUIP,"","d:/ymir work/effect/etc/buff/buff_item7.mse")
|
||||
chrmgr.RegisterEffect(chrmgr.EFFECT_REFINED+chrmgr.EFFECT_SASH,"Bip01","d:/ymir work/pc/common/effect/armor/acc_01.mse")
|
||||
|
||||
|
||||
_open=game.GameWindow.Open
|
||||
_close=game.GameWindow.Close
|
||||
_command=game.GameWindow.BINARY_ServerCommand_Run
|
||||
|
||||
def Open(self):
|
||||
global g_windows
|
||||
_open(self)
|
||||
RegisterEffects()
|
||||
g_windows=(CombineWindow(),AbsorbWindow())
|
||||
for window in g_windows:
|
||||
window.LoadWindow();window.Hide()
|
||||
if self.interface:window.SetItemToolTip(self.interface.tooltipItem)
|
||||
|
||||
def Close(self):
|
||||
global g_windows
|
||||
if g_windows:
|
||||
for window in g_windows:window.Close();window.Destroy()
|
||||
g_windows=None
|
||||
_close(self)
|
||||
|
||||
def ActSash(self,iAct,bWindow):
|
||||
if not g_windows or not self.interface:return
|
||||
window=g_windows[0 if bWindow else 1]
|
||||
inventory=self.interface.wndInventory
|
||||
if iAct==1:
|
||||
if not window.IsOpened():window.Open()
|
||||
if inventory and not inventory.IsShow():inventory.Show()
|
||||
elif iAct==2:
|
||||
if window.IsOpened():window.Close()
|
||||
elif iAct==3 or iAct==4:
|
||||
if window.IsOpened():window.Refresh(iAct)
|
||||
if inventory:inventory.RefreshBagSlotWindow()
|
||||
|
||||
def AlertSash(self,bWindow):
|
||||
self.PopupMessage(L(ALERT_COMBINE if bWindow else ALERT_ABSORB))
|
||||
|
||||
def Command(self,line):
|
||||
if line.startswith('MTSASH '):
|
||||
words=line.split()
|
||||
if len(words)==3 and words[1]=='abs' and words[2].isdigit():text=ABS_CHANGED%int(words[2])
|
||||
else:text=MESSAGES.get(words[1] if len(words)>1 else '')
|
||||
if text:chat.AppendChat(chat.CHAT_TYPE_INFO,L(text))
|
||||
return 1
|
||||
return _command(self,line)
|
||||
|
||||
game.GameWindow.Open=Open
|
||||
game.GameWindow.Close=Close
|
||||
game.GameWindow.ActSash=ActSash
|
||||
game.GameWindow.AlertSash=AlertSash
|
||||
game.GameWindow.BINARY_ServerCommand_Run=Command
|
||||
)PY";
|
||||
@@ -188,9 +188,11 @@ class SwitchbotWindow(ui.Window):
|
||||
vnum=player.GetItemIndex(player.INVENTORY,cell)
|
||||
if not vnum:continue
|
||||
item.SelectItem(vnum);kind=item.GetItemType()
|
||||
if kind==item.ITEM_TYPE_USE and item.GetItemSubType()==USE_CHANGE_ATTRIBUTE:
|
||||
# 54001-54267 with a value2 mask are AVA2 costume enchants (mt_costume_attr), 71302 / 53996 talisman
|
||||
# rerolls (mt_pendant), not switchers; talismans have their own attribute pool.
|
||||
if kind==item.ITEM_TYPE_USE and item.GetItemSubType()==USE_CHANGE_ATTRIBUTE and not (54001<=vnum<=54267 and item.GetValue(2)) and vnum not in (71302,53996):
|
||||
scrolls[vnum]=scrolls.get(vnum,0)+player.GetItemCount(player.INVENTORY,cell)
|
||||
elif kind in (item.ITEM_TYPE_WEAPON,item.ITEM_TYPE_ARMOR) and any(player.GetItemAttribute(player.INVENTORY,cell,i)[0] for i in range(5)):
|
||||
elif kind in (item.ITEM_TYPE_WEAPON,item.ITEM_TYPE_ARMOR) and not (kind==item.ITEM_TYPE_ARMOR and item.GetItemSubType()==item.ARMOR_PENDANT) and any(player.GetItemAttribute(player.INVENTORY,cell,i)[0] for i in range(5)):
|
||||
items.append((cell,vnum))
|
||||
selected=self.Selected();scroll=self.Scroll();state=self.bot.state
|
||||
if state and state['active'] and (state['cell'],state['vnum']) in items:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -755,6 +760,12 @@ bool LoadLocaleData(const char* localePath)
|
||||
Tracenf("LoadLocaleData - LoadItemDesc(%s) Error", szItemDesc);
|
||||
}
|
||||
|
||||
// PORT: AVA2 sash part scale (ENABLE_SASH_SYSTEM)
|
||||
char szItemScale[256];
|
||||
snprintf(szItemScale, sizeof(szItemScale), "%s/item_scale.txt", localePath);
|
||||
if (!rkItemMgr.LoadItemScale(szItemScale))
|
||||
Tracenf("LoadLocaleData: error while loading %s.", szItemScale);
|
||||
|
||||
if (!rkNPCMgr.LoadNonPlayerData(szMobProto))
|
||||
{
|
||||
TraceError("LoadLocaleData - LoadMobProto(%s) Error", szMobProto);
|
||||
|
||||
@@ -328,12 +328,16 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
|
||||
COMMAND ${MT_WIKI_PYTHON3} ${PROJECT_SOURCE_DIR}/tests/wiki/test_boss.py)
|
||||
add_test(NAME port.chest
|
||||
COMMAND ${MT_WIKI_PYTHON3} ${PROJECT_SOURCE_DIR}/tests/wiki/test_chest.py)
|
||||
add_test(NAME port.ava2_items
|
||||
COMMAND ${MT_WIKI_PYTHON3} ${PROJECT_SOURCE_DIR}/tools/server/ava2-items/tests/test_items.py)
|
||||
endif()
|
||||
# Offline locale data builder: retains 40250 proto layout and gameplay fields.
|
||||
add_executable(locale_proto_patch ${PROJECT_SOURCE_DIR}/tests/port/locale_proto_patch.cpp)
|
||||
target_link_libraries(locale_proto_patch PRIVATE port_platform)
|
||||
add_executable(biolog_proto_patch ${PROJECT_SOURCE_DIR}/tests/port/biolog_proto_patch.cpp)
|
||||
target_link_libraries(biolog_proto_patch PRIVATE port_platform)
|
||||
add_executable(item_proto_patch ${PROJECT_SOURCE_DIR}/tests/port/item_proto_patch.cpp)
|
||||
target_link_libraries(item_proto_patch PRIVATE port_platform)
|
||||
# Locale packs in the original format (scripts/build_zh_locale.py packs locale/zh with it).
|
||||
add_executable(eterpack_pack ${PROJECT_SOURCE_DIR}/tests/port/eterpack_pack.cpp)
|
||||
target_link_libraries(eterpack_pack PRIVATE port_platform)
|
||||
|
||||
@@ -685,21 +685,50 @@ void CGraphicThingInstance::DeformAll()
|
||||
{
|
||||
m_bUpdated = true;
|
||||
|
||||
CGrannyLODController::FDeformAll deform;
|
||||
deform.mc_pWorldMatrix = &m_worldMatrix;
|
||||
std::for_each(m_LODControllerVector.begin(), m_LODControllerVector.end(), deform);
|
||||
D3DXMATRIX matScaled;
|
||||
for (int i = 0; i < (int)m_LODControllerVector.size(); ++i)
|
||||
{
|
||||
CGrannyLODController* pkLOD = m_LODControllerVector[i];
|
||||
if (pkLOD->isModelInstance())
|
||||
pkLOD->DeformAll(__GetPartWorldMatrix(i, matScaled));
|
||||
}
|
||||
}
|
||||
|
||||
void CGraphicThingInstance::SetPartScale(int iPart, float fX, float fY, float fZ)
|
||||
{
|
||||
D3DXMATRIX matScale;
|
||||
D3DXMatrixScaling(&matScale, fX, fY, fZ);
|
||||
m_partScaleMap[iPart] = matScale;
|
||||
}
|
||||
|
||||
void CGraphicThingInstance::ClearPartScale(int iPart)
|
||||
{
|
||||
m_partScaleMap.erase(iPart);
|
||||
}
|
||||
|
||||
const D3DXMATRIX * CGraphicThingInstance::__GetPartWorldMatrix(int iPart, D3DXMATRIX & rScaled) const
|
||||
{
|
||||
if (m_partScaleMap.empty())
|
||||
return &m_worldMatrix;
|
||||
|
||||
std::map<int, D3DXMATRIX>::const_iterator it = m_partScaleMap.find(iPart);
|
||||
if (it == m_partScaleMap.end())
|
||||
return &m_worldMatrix;
|
||||
|
||||
D3DXMatrixMultiply(&rScaled, &it->second, &m_worldMatrix);
|
||||
return &rScaled;
|
||||
}
|
||||
|
||||
void CGraphicThingInstance::DeformNoSkin()
|
||||
{
|
||||
m_bUpdated = true;
|
||||
|
||||
std::vector<CGrannyLODController*>::iterator i;
|
||||
for (i=m_LODControllerVector.begin(); i!=m_LODControllerVector.end(); ++i)
|
||||
D3DXMATRIX matScaled;
|
||||
for (int i = 0; i < (int)m_LODControllerVector.size(); ++i)
|
||||
{
|
||||
CGrannyLODController* pkLOD=*i;
|
||||
CGrannyLODController* pkLOD=m_LODControllerVector[i];
|
||||
if (pkLOD->isModelInstance())
|
||||
pkLOD->DeformNoSkin(&m_worldMatrix);
|
||||
pkLOD->DeformNoSkin(__GetPartWorldMatrix(i, matScaled));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -707,9 +736,13 @@ void CGraphicThingInstance::OnDeform()
|
||||
{
|
||||
m_bUpdated = true;
|
||||
|
||||
CGrannyLODController::FDeform deform;
|
||||
deform.mc_pWorldMatrix = &m_worldMatrix;
|
||||
std::for_each(m_LODControllerVector.begin(), m_LODControllerVector.end(), deform);
|
||||
D3DXMATRIX matScaled;
|
||||
for (int i = 0; i < (int)m_LODControllerVector.size(); ++i)
|
||||
{
|
||||
CGrannyLODController* pkLOD = m_LODControllerVector[i];
|
||||
if (pkLOD->isModelInstance())
|
||||
pkLOD->Deform(__GetPartWorldMatrix(i, matScaled));
|
||||
}
|
||||
}
|
||||
|
||||
void CGraphicThingInstance::__SetLocalTime(float fLocalTime)
|
||||
@@ -938,6 +971,7 @@ void CGraphicThingInstance::OnClear()
|
||||
{
|
||||
stl_wipe(m_LODControllerVector);
|
||||
stl_wipe_second(m_roMotionThingMap);
|
||||
m_partScaleMap.clear();
|
||||
|
||||
for (DWORD d = 0; d < m_modelThingSetVector.size(); ++d)
|
||||
m_modelThingSetVector[d].Clear();
|
||||
|
||||
@@ -34,6 +34,11 @@ class CGraphicThingInstance : public CGraphicObjectInstance
|
||||
|
||||
void UpdateLODLevel();
|
||||
void UpdateTime();
|
||||
// PORT: AVA2 ENABLE_OBJ_SCALLING for the sash part: the part is deformed with Scale * world,
|
||||
// i.e. scaled about the actor origin in actor space (item_scale.txt).
|
||||
void SetPartScale(int iPart, float fX, float fY, float fZ);
|
||||
void ClearPartScale(int iPart);
|
||||
|
||||
void DeformAll(); // 모든 LOD 디폼
|
||||
|
||||
bool LessRenderOrder(CGraphicThingInstance* pkThingInst);
|
||||
@@ -139,6 +144,9 @@ class CGraphicThingInstance : public CGraphicObjectInstance
|
||||
std::vector<CGrannyLODController *> m_LODControllerVector;
|
||||
std::vector<TModelThingSet> m_modelThingSetVector;
|
||||
std::map<DWORD, CGraphicThing::TRef *> m_roMotionThingMap;
|
||||
std::map<int, D3DXMATRIX> m_partScaleMap; // PORT: AVA2 sash scale
|
||||
|
||||
const D3DXMATRIX * __GetPartWorldMatrix(int iPart, D3DXMATRIX & rScaled) const;
|
||||
|
||||
protected:
|
||||
virtual void OnUpdateCollisionData(const CStaticCollisionDataVector * pscdVector);
|
||||
|
||||
@@ -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{};
|
||||
};
|
||||
@@ -243,6 +243,7 @@ class CActorInstance : public IActorInstance, public IFlyTargetableObject
|
||||
|
||||
bool SetRace(DWORD eRace);
|
||||
void SetHair(DWORD eHair);
|
||||
void AttachSash(CItemData * pItemData, float fSpecular = 0.0f); // PORT: AVA2 sash
|
||||
void SetVirtualID(DWORD dwVID);
|
||||
|
||||
void SetShape(DWORD eShape, float fSpecular=0.0f);
|
||||
|
||||
@@ -624,3 +624,44 @@ void CActorInstance::__ClearAttachingEffect()
|
||||
}
|
||||
m_AttachingEffectList.clear();
|
||||
}
|
||||
|
||||
// PORT: AVA2 ActorInstanceAttach.cpp AttachSash (ENABLE_SASH_SYSTEM). The sash is a model part on
|
||||
// "Bip01 Spine2" that plays its own first motion (the cloth sway).
|
||||
void CActorInstance::AttachSash(CItemData * pItemData, float fSpecular)
|
||||
{
|
||||
if (!pItemData)
|
||||
{
|
||||
RegisterModelThing(CRaceData::PART_SASH, NULL);
|
||||
SetModelInstance(CRaceData::PART_SASH, CRaceData::PART_SASH, 0);
|
||||
RefreshActorInstance();
|
||||
return;
|
||||
}
|
||||
|
||||
RegisterModelThing(CRaceData::PART_SASH, pItemData->GetModelThing());
|
||||
SetModelInstance(CRaceData::PART_SASH, CRaceData::PART_SASH, 0);
|
||||
AttachModelInstance(CRaceData::PART_MAIN, "Bip01 Spine2", CRaceData::PART_SASH);
|
||||
|
||||
if (fSpecular > 0.0f)
|
||||
{
|
||||
SMaterialData kMaterialData;
|
||||
kMaterialData.pImage = NULL;
|
||||
kMaterialData.isSpecularEnable = TRUE;
|
||||
kMaterialData.fSpecularPower = fSpecular;
|
||||
kMaterialData.bSphereMapIndex = 1;
|
||||
SetMaterialData(CRaceData::PART_SASH, NULL, kMaterialData);
|
||||
}
|
||||
|
||||
if (CRaceData::PART_SASH >= m_LODControllerVector.size())
|
||||
return;
|
||||
|
||||
if (CGrannyLODController* pLODController = m_LODControllerVector[CRaceData::PART_SASH])
|
||||
{
|
||||
if (CGrannyModelInstance* pSashModelInstance = pLODController->GetModelInstance())
|
||||
{
|
||||
CGraphicThing* pItemGraphicThing = pItemData->GetModelThing();
|
||||
if (pItemGraphicThing && pItemGraphicThing->GetMotionCount() > 0)
|
||||
if (CGrannyMotion* pItemMotion = pItemGraphicThing->GetMotionPointer(0))
|
||||
pSashModelInstance->SetMotionPointer(pItemMotion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +215,43 @@ void CItemData::SetItemTableData(TItemTable * pItemTable)
|
||||
memcpy(&m_ItemTable, pItemTable, sizeof(TItemTable));
|
||||
}
|
||||
|
||||
// PORT: AVA2 ItemData.cpp SetItemScale/GetItemScale. The position columns are dropped: AVA2 feeds
|
||||
// them to SetScalePosition, which its deform never reads. JOB_WOLFMAN rows are skipped (no wolfman).
|
||||
void CItemData::SetItemScale(const std::string & strJob, const std::string & strSex, const std::string & strScaleX, const std::string & strScaleY, const std::string & strScaleZ)
|
||||
{
|
||||
DWORD dwPos;
|
||||
if (strJob == "JOB_WARRIOR")
|
||||
dwPos = NRaceData::JOB_WARRIOR;
|
||||
else if (strJob == "JOB_ASSASSIN")
|
||||
dwPos = NRaceData::JOB_ASSASSIN;
|
||||
else if (strJob == "JOB_SURA")
|
||||
dwPos = NRaceData::JOB_SURA;
|
||||
else if (strJob == "JOB_SHAMAN")
|
||||
dwPos = NRaceData::JOB_SHAMAN;
|
||||
else
|
||||
return;
|
||||
|
||||
dwPos += 1;
|
||||
if (strSex == "F")
|
||||
dwPos += 5;
|
||||
|
||||
m_aScaleInfo[dwPos].bSet = true;
|
||||
m_aScaleInfo[dwPos].fScaleX = float(atof(strScaleX.c_str()) / 100.0f);
|
||||
m_aScaleInfo[dwPos].fScaleY = float(atof(strScaleY.c_str()) / 100.0f);
|
||||
m_aScaleInfo[dwPos].fScaleZ = float(atof(strScaleZ.c_str()) / 100.0f);
|
||||
}
|
||||
|
||||
bool CItemData::GetItemScale(DWORD dwPos, float & fScaleX, float & fScaleY, float & fScaleZ)
|
||||
{
|
||||
if (dwPos >= sizeof(m_aScaleInfo) / sizeof(m_aScaleInfo[0]) || !m_aScaleInfo[dwPos].bSet)
|
||||
return false;
|
||||
|
||||
fScaleX = m_aScaleInfo[dwPos].fScaleX;
|
||||
fScaleY = m_aScaleInfo[dwPos].fScaleY;
|
||||
fScaleZ = m_aScaleInfo[dwPos].fScaleZ;
|
||||
return true;
|
||||
}
|
||||
|
||||
const CItemData::TItemTable* CItemData::GetTable() const
|
||||
{
|
||||
return &m_ItemTable;
|
||||
@@ -452,6 +489,7 @@ void CItemData::Clear()
|
||||
m_pLODModelThingVector.clear();
|
||||
|
||||
memset(&m_ItemTable, 0, sizeof(m_ItemTable));
|
||||
memset(m_aScaleInfo, 0, sizeof(m_aScaleInfo));
|
||||
}
|
||||
|
||||
CItemData::CItemData()
|
||||
|
||||
@@ -56,6 +56,8 @@ class CItemData
|
||||
ITEM_TYPE_SECONDARY_COIN, //32 명도전.
|
||||
ITEM_TYPE_RING, //33 반지 (유니크 슬롯이 아닌 순수 반지 슬롯)
|
||||
ITEM_TYPE_BELT, //34 벨트
|
||||
ITEM_TYPE_SHINING, //35 PORT: AVA2 shining (not ported, keeps the AVA2 numbering)
|
||||
ITEM_TYPE_RINGS, //36 PORT: AVA2 new rings, worn in c_Equipment_Rings1/2
|
||||
|
||||
ITEM_TYPE_MAX_NUM,
|
||||
};
|
||||
@@ -95,6 +97,7 @@ class CItemData
|
||||
ARMOR_FOOTS,
|
||||
ARMOR_NECK,
|
||||
ARMOR_EAR,
|
||||
ARMOR_PENDANT, // PORT: AVA2 talisman, worn in c_Equipment_Pendant
|
||||
ARMOR_NUM_TYPES
|
||||
};
|
||||
|
||||
@@ -102,6 +105,9 @@ class CItemData
|
||||
{
|
||||
COSTUME_BODY, //0 갑옷(main look)
|
||||
COSTUME_HAIR, //1 헤어(탈착가능)
|
||||
COSTUME_MOUNT = 2, // PORT: AVA2 mount costume (value0 = horse race)
|
||||
COSTUME_SASH = 3, // PORT: AVA2 sash
|
||||
COSTUME_MOUNT_SKIN = 4, // PORT: AVA2 mount skin (needs a worn COSTUME_MOUNT)
|
||||
COSTUME_NUM_TYPES,
|
||||
};
|
||||
|
||||
@@ -350,6 +356,16 @@ class CItemData
|
||||
APPLY_ANTI_CRITICAL_PCT, //90 크리티컬 저항
|
||||
APPLY_ANTI_PENETRATE_PCT, //91 관통타격 저항
|
||||
|
||||
// PORT AVA2 bonus numbering, matching the server (common/length.h): 92-122 and 129-138 are not ported yet.
|
||||
APPLY_ENCHANT_ELEC = 123, // talisman: attack +x% on monsters of that element
|
||||
APPLY_ENCHANT_FIRE = 124,
|
||||
APPLY_ENCHANT_ICE = 125,
|
||||
APPLY_ENCHANT_WIND = 126,
|
||||
APPLY_ENCHANT_EARTH = 127,
|
||||
APPLY_ENCHANT_DARK = 128,
|
||||
APPLY_ATTBONUS_STONE = 139,
|
||||
APPLY_ATTBONUS_BOSS = 140,
|
||||
|
||||
MAX_APPLY_NUM, //
|
||||
};
|
||||
|
||||
@@ -491,6 +507,9 @@ class CItemData
|
||||
//BOOL LoadItemData(const char * c_szFileName);
|
||||
void SetDefaultItemData(const char * c_szIconFileName, const char * c_szModelFileName = NULL);
|
||||
void SetItemTableData(TItemTable * pItemTable);
|
||||
// PORT: AVA2 item_scale.txt (sash part scale per job/sex). Slot = job + 1, + 5 when female.
|
||||
void SetItemScale(const std::string & strJob, const std::string & strSex, const std::string & strScaleX, const std::string & strScaleY, const std::string & strScaleZ);
|
||||
bool GetItemScale(DWORD dwPos, float & fScaleX, float & fScaleY, float & fScaleZ);
|
||||
|
||||
protected:
|
||||
void __LoadFiles();
|
||||
@@ -514,6 +533,12 @@ class CItemData
|
||||
NRaceData::TAttachingDataVector m_AttachingDataVector;
|
||||
DWORD m_dwVnum;
|
||||
TItemTable m_ItemTable;
|
||||
struct SScaleInfo
|
||||
{
|
||||
bool bSet;
|
||||
float fScaleX, fScaleY, fScaleZ;
|
||||
};
|
||||
SScaleInfo m_aScaleInfo[11];
|
||||
|
||||
public:
|
||||
static void DestroySystem();
|
||||
|
||||
@@ -402,3 +402,39 @@ CItemManager::~CItemManager()
|
||||
{
|
||||
Destroy();
|
||||
}
|
||||
|
||||
// PORT: AVA2 ItemManager.cpp LoadItemScale (ENABLE_SASH_SYSTEM). Each row covers vnum .. vnum + 4;
|
||||
// unlike AVA2 (MakeItemData) only vnums already in the item table are touched.
|
||||
bool CItemManager::LoadItemScale(const char * c_szFileName)
|
||||
{
|
||||
const VOID* pvData;
|
||||
CMappedFile kFile;
|
||||
if (!CEterPackManager::Instance().Get(kFile, c_szFileName, &pvData))
|
||||
return false;
|
||||
|
||||
CMemoryTextFileLoader kTextFileLoader;
|
||||
kTextFileLoader.Bind(kFile.Size(), pvData);
|
||||
|
||||
CTokenVector kTokenVector;
|
||||
for (DWORD i = 0; i < kTextFileLoader.GetLineCount(); ++i)
|
||||
{
|
||||
if (!kTextFileLoader.SplitLineByTab(i, &kTokenVector))
|
||||
continue;
|
||||
|
||||
if (kTokenVector.size() < ITEMSCALE_NUM)
|
||||
{
|
||||
TraceError("LoadItemScale: invalid line %d (%s).", i, c_szFileName);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int j = 0; j < 5; ++j)
|
||||
{
|
||||
TItemMap::iterator f = m_ItemMap.find(atoi(kTokenVector[ITEMSCALE_VNUM].c_str()) + j);
|
||||
if (m_ItemMap.end() == f)
|
||||
continue;
|
||||
f->second->SetItemScale(kTokenVector[ITEMSCALE_JOB], kTokenVector[ITEMSCALE_SEX], kTokenVector[ITEMSCALE_SCALE_X], kTokenVector[ITEMSCALE_SCALE_Y], kTokenVector[ITEMSCALE_SCALE_Z]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,20 @@ class CItemManager : public CSingleton<CItemManager>
|
||||
ITEMDESC_COL_SUMM,
|
||||
ITEMDESC_COL_NUM,
|
||||
};
|
||||
// PORT: AVA2 item_scale.txt columns
|
||||
enum EItemScaleColumn
|
||||
{
|
||||
ITEMSCALE_VNUM,
|
||||
ITEMSCALE_JOB,
|
||||
ITEMSCALE_SEX,
|
||||
ITEMSCALE_SCALE_X,
|
||||
ITEMSCALE_SCALE_Y,
|
||||
ITEMSCALE_SCALE_Z,
|
||||
ITEMSCALE_POSITION_X,
|
||||
ITEMSCALE_POSITION_Y,
|
||||
ITEMSCALE_POSITION_Z,
|
||||
ITEMSCALE_NUM,
|
||||
};
|
||||
|
||||
public:
|
||||
typedef std::map<DWORD, CItemData*> TItemMap;
|
||||
@@ -31,6 +45,7 @@ class CItemManager : public CSingleton<CItemManager>
|
||||
|
||||
/////
|
||||
bool LoadItemDesc(const char* c_szFileName);
|
||||
bool LoadItemScale(const char* c_szFileName); // PORT: AVA2 sash
|
||||
bool LoadItemList(const char* c_szFileName);
|
||||
bool LoadItemTable(const char* c_szFileName);
|
||||
CItemData * MakeItemData(DWORD dwIndex);
|
||||
|
||||
@@ -23,6 +23,7 @@ class CRaceData
|
||||
PART_HEAD,
|
||||
PART_WEAPON_LEFT,
|
||||
PART_HAIR,
|
||||
PART_SASH, // PORT: AVA2 sash ("Bip01 Spine2", item vnum - 85000)
|
||||
|
||||
PART_MAX_NUM,
|
||||
};
|
||||
|
||||
@@ -22,6 +22,15 @@ bool __IsNPCRace(unsigned race)
|
||||
return false;
|
||||
}
|
||||
|
||||
// PORT: AVA2 __IsNewMountRace — mount costume / skin races (christmas_2017_sleddog, ...) live in npc_mount.
|
||||
bool __IsNewMountRace(unsigned race)
|
||||
{
|
||||
if (race >= 32000 && race <= 32100)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void __GetRaceResourcePathes(unsigned race, std::vector <std::string>& vec_stPathes)
|
||||
{
|
||||
if (__IsGuildRace(race))
|
||||
@@ -37,6 +46,8 @@ void __GetRaceResourcePathes(unsigned race, std::vector <std::string>& vec_stPat
|
||||
if (race >= 30000)
|
||||
{
|
||||
vec_stPathes.push_back ("d:/ymir work/npc2/");
|
||||
if (__IsNewMountRace(race)) // PORT: AVA2
|
||||
vec_stPathes.push_back ("d:/ymir work/npc_mount/");
|
||||
vec_stPathes.push_back ("d:/ymir work/npc/");
|
||||
vec_stPathes.push_back ("d:/ymir work/monster/");
|
||||
vec_stPathes.push_back ("d:/ymir work/monster2/");
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -104,7 +104,13 @@ enum EDragonSoulStepTypes
|
||||
const DWORD c_Costume_Slot_Hair = c_Costume_Slot_Start + 1;
|
||||
const DWORD c_Costume_Slot_Count = 2;
|
||||
const DWORD c_Costume_Slot_End = c_Costume_Slot_Start + c_Costume_Slot_Count;
|
||||
const DWORD c_Costume_Slot_Sash = c_Equipment_Start + 24; // PORT: AVA2 sash, server WEAR_COSTUME_SASH (after WEAR_BELT)
|
||||
const DWORD c_Costume_Slot_Mount = c_Equipment_Start + 25; // PORT: AVA2 mount costume, server WEAR_COSTUME_MOUNT
|
||||
const DWORD c_Costume_Slot_MountSkin = c_Equipment_Start + 26; // PORT: AVA2 mount skin, server WEAR_COSTUME_MOUNT_SKIN
|
||||
#endif
|
||||
const DWORD c_Equipment_Pendant = c_Equipment_Start + 27; // PORT: AVA2 talisman, server WEAR_PENDANT
|
||||
const DWORD c_Equipment_Rings1 = c_Equipment_Start + 28; // PORT: AVA2 new rings, server WEAR_RINGS_1/2
|
||||
const DWORD c_Equipment_Rings2 = c_Equipment_Start + 29;
|
||||
|
||||
|
||||
// [주의] 숫자(32) 하드코딩 주의. 현재 서버에서 용혼석 슬롯은 32부터임.
|
||||
|
||||
@@ -756,6 +756,7 @@ bool CInstanceBase::Create(const SCreateData& c_rkCreateData)
|
||||
{
|
||||
SetHair(c_rkCreateData.m_dwHair);
|
||||
SetWeapon(c_rkCreateData.m_dwWeapon);
|
||||
SetSash(c_rkCreateData.m_dwSash); // PORT: AVA2 sash
|
||||
}
|
||||
|
||||
__Create_SetName(c_rkCreateData);
|
||||
@@ -2592,6 +2593,80 @@ void CInstanceBase::SetHair(DWORD eHair)
|
||||
m_GraphicThingInstance.SetHair(eHair);
|
||||
}
|
||||
|
||||
// PORT: AVA2 InstanceBase.cpp SetSash/ChangeSash/ClearSashEffect (ENABLE_SASH_SYSTEM + ENABLE_OBJ_SCALLING).
|
||||
// Differences: part 0 detaches the model (AVA2 looked up item 85000 and returned early, leaving the old
|
||||
// sash on), and only the item_scale.txt scale is applied (its position columns never reached the deform).
|
||||
void CInstanceBase::SetSash(DWORD dwSash)
|
||||
{
|
||||
if (!IsPC() && GetRace() > 8)
|
||||
return;
|
||||
|
||||
if (IsPC() && IsPoly())
|
||||
return;
|
||||
|
||||
m_awPart[CRaceData::PART_SASH] = dwSash;
|
||||
|
||||
ClearSashEffect();
|
||||
m_GraphicThingInstance.ClearPartScale(CRaceData::PART_SASH);
|
||||
|
||||
if (!dwSash)
|
||||
{
|
||||
m_GraphicThingInstance.AttachSash(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
dwSash += 85000;
|
||||
|
||||
float fSpecular = 65.0f;
|
||||
if (dwSash > 86000)
|
||||
{
|
||||
fSpecular += 35;
|
||||
|
||||
m_dwSashEffect = EFFECT_REFINED + EFFECT_SASH;
|
||||
__EffectContainer_AttachEffect(m_dwSashEffect);
|
||||
}
|
||||
|
||||
fSpecular /= 100.0f;
|
||||
|
||||
CItemData * pItemData;
|
||||
if (!CItemManager::Instance().GetItemDataPointer(dwSash, &pItemData))
|
||||
{
|
||||
m_GraphicThingInstance.AttachSash(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
m_GraphicThingInstance.AttachSash(pItemData, fSpecular);
|
||||
|
||||
DWORD dwRace = GetRace(), dwPos = RaceToJob(dwRace), dwSex = RaceToSex(dwRace);
|
||||
dwPos += 1;
|
||||
if (dwSex == 0)
|
||||
dwPos += 5;
|
||||
|
||||
float fScaleX, fScaleY, fScaleZ;
|
||||
if (pItemData->GetItemScale(dwPos, fScaleX, fScaleY, fScaleZ))
|
||||
m_GraphicThingInstance.SetPartScale(CRaceData::PART_SASH, fScaleX, fScaleY, fScaleZ);
|
||||
}
|
||||
|
||||
void CInstanceBase::ChangeSash(DWORD dwSash)
|
||||
{
|
||||
if (!IsPC() && GetRace() > 7)
|
||||
return;
|
||||
|
||||
if (GetPart(CRaceData::PART_SASH) == dwSash)
|
||||
return;
|
||||
|
||||
SetSash(dwSash);
|
||||
}
|
||||
|
||||
void CInstanceBase::ClearSashEffect()
|
||||
{
|
||||
if (!m_dwSashEffect)
|
||||
return;
|
||||
|
||||
__EffectContainer_DetachEffect(m_dwSashEffect);
|
||||
m_dwSashEffect = 0;
|
||||
}
|
||||
|
||||
void CInstanceBase::ChangeHair(DWORD eHair)
|
||||
{
|
||||
if (!HAIR_COLOR_ENABLE)
|
||||
@@ -2815,6 +2890,7 @@ bool CInstanceBase::ChangeArmor(DWORD dwArmor)
|
||||
DWORD dwVID = GetVirtualID();
|
||||
DWORD dwRace = GetRace();
|
||||
DWORD eHair = GetPart(CRaceData::PART_HAIR);
|
||||
DWORD dwSash = GetPart(CRaceData::PART_SASH); // PORT: AVA2 sash
|
||||
DWORD eWeapon = GetPart(CRaceData::PART_WEAPON);
|
||||
float fRot = GetRotation();
|
||||
float fAdvRot = GetAdvancingRotation();
|
||||
@@ -2835,6 +2911,7 @@ bool CInstanceBase::ChangeArmor(DWORD dwArmor)
|
||||
|
||||
SetArmor(dwArmor);
|
||||
SetHair(eHair);
|
||||
SetSash(dwSash); // PORT: AVA2 sash
|
||||
SetWeapon(eWeapon);
|
||||
|
||||
SetRotation(fRot);
|
||||
@@ -3102,6 +3179,7 @@ void CInstanceBase::__Initialize()
|
||||
m_swordRefineEffectRight = 0;
|
||||
m_swordRefineEffectLeft = 0;
|
||||
m_armorRefineEffect = 0;
|
||||
m_dwSashEffect = 0; // PORT: AVA2 sash
|
||||
|
||||
m_sAlignment = 0;
|
||||
m_byPKMode = 0;
|
||||
|
||||
@@ -26,6 +26,7 @@ class CInstanceBase
|
||||
DWORD m_dwWeapon;
|
||||
DWORD m_dwHair;
|
||||
DWORD m_dwMountVnum;
|
||||
DWORD m_dwSash; // PORT: AVA2 sash part (item vnum - 85000)
|
||||
|
||||
short m_sAlignment;
|
||||
BYTE m_byPKMode;
|
||||
@@ -293,6 +294,7 @@ class CInstanceBase
|
||||
|
||||
EFFECT_BODYARMOR_SPECIAL, // 갑옷 4-2-1
|
||||
EFFECT_BODYARMOR_SPECIAL2, // 갑옷 4-2-2
|
||||
EFFECT_SASH, // PORT: AVA2 refined sash glow (acc_01.mse)
|
||||
|
||||
EFFECT_REFINED_NUM,
|
||||
};
|
||||
@@ -370,6 +372,8 @@ class CInstanceBase
|
||||
EFFECT_HALLOWEEN_CANDY_EQUIP, // 할로윈 사탕 착용 순간에 발동하는 이펙트
|
||||
EFFECT_HAPPINESS_RING_EQUIP, // 행복의 반지 착용 순간에 발동하는 이펙트
|
||||
EFFECT_LOVE_PENDANT_EQUIP, // 행복의 반지 착용 순간에 발동하는 이펙트
|
||||
EFFECT_SASH_SUCCEDED, // PORT: AVA2 sash
|
||||
EFFECT_SASH_EQUIP,
|
||||
EFFECT_TEMP,
|
||||
EFFECT_NUM,
|
||||
};
|
||||
@@ -500,6 +504,8 @@ class CInstanceBase
|
||||
bool ChangeArmor(DWORD dwArmor);
|
||||
void ChangeWeapon(DWORD eWeapon);
|
||||
void ChangeHair(DWORD eHair);
|
||||
void SetSash(DWORD dwSash); // PORT: AVA2 sash
|
||||
void ChangeSash(DWORD dwSash);
|
||||
void ChangeGuild(DWORD dwGuildID);
|
||||
DWORD GetWeaponType();
|
||||
|
||||
@@ -909,6 +915,7 @@ class CInstanceBase
|
||||
UINT __GetRefinedEffect(CItemData* pItem);
|
||||
void __ClearWeaponRefineEffect();
|
||||
void __ClearArmorRefineEffect();
|
||||
void ClearSashEffect(); // PORT: AVA2 sash
|
||||
|
||||
protected:
|
||||
void __AttachSelectEffect();
|
||||
@@ -977,6 +984,7 @@ class CInstanceBase
|
||||
DWORD m_swordRefineEffectRight;
|
||||
DWORD m_swordRefineEffectLeft;
|
||||
DWORD m_armorRefineEffect;
|
||||
DWORD m_dwSashEffect; // PORT: AVA2 sash
|
||||
|
||||
struct SMoveAfterFunc
|
||||
{
|
||||
|
||||
@@ -77,6 +77,7 @@ void SNetworkActorData::__copy__(const SNetworkActorData& src)
|
||||
m_dwArmor = src.m_dwArmor;
|
||||
m_dwWeapon = src.m_dwWeapon;
|
||||
m_dwHair = src.m_dwHair;
|
||||
m_dwSash = src.m_dwSash; // PORT: AVA2 sash
|
||||
|
||||
m_dwOwnerVID = src.m_dwOwnerVID;
|
||||
|
||||
@@ -104,6 +105,7 @@ SNetworkActorData::SNetworkActorData()
|
||||
m_dwArmor=0;
|
||||
m_dwWeapon=0;
|
||||
m_dwHair=0;
|
||||
m_dwSash=0; // PORT: AVA2 sash
|
||||
m_dwEmpireID=0;
|
||||
|
||||
m_dwOwnerVID=0;
|
||||
@@ -356,6 +358,7 @@ CInstanceBase* CNetworkActorManager::__AppendCharacterManagerActor(SNetworkActor
|
||||
kCreateData.m_dwArmor=rkNetActorData.m_dwArmor;
|
||||
kCreateData.m_dwWeapon=rkNetActorData.m_dwWeapon;
|
||||
kCreateData.m_dwHair=rkNetActorData.m_dwHair;
|
||||
kCreateData.m_dwSash=rkNetActorData.m_dwSash; // PORT: AVA2 sash
|
||||
kCreateData.m_isMain=__IsMainActorVID(dwVID);
|
||||
|
||||
CInstanceBase* pOldInstance = rkChrMgr.GetInstancePtr(dwVID);
|
||||
@@ -473,6 +476,7 @@ void CNetworkActorManager::UpdateActor(const SNetworkUpdateActorData& c_rkNetUpd
|
||||
pkInstFind->ChangeArmor(c_rkNetUpdateActorData.m_dwArmor);
|
||||
pkInstFind->ChangeWeapon(c_rkNetUpdateActorData.m_dwWeapon);
|
||||
pkInstFind->ChangeHair(c_rkNetUpdateActorData.m_dwHair);
|
||||
pkInstFind->ChangeSash(c_rkNetUpdateActorData.m_dwSash); // PORT: AVA2 sash
|
||||
pkInstFind->ChangeGuild(c_rkNetUpdateActorData.m_dwGuildID);
|
||||
pkInstFind->SetAffectFlagContainer(c_rkNetUpdateActorData.m_kAffectFlags);
|
||||
pkInstFind->SetMoveSpeed(c_rkNetUpdateActorData.m_dwMovSpd);
|
||||
@@ -502,6 +506,7 @@ void CNetworkActorManager::UpdateActor(const SNetworkUpdateActorData& c_rkNetUpd
|
||||
rkNetActorData.m_dwArmor=c_rkNetUpdateActorData.m_dwArmor;
|
||||
rkNetActorData.m_dwWeapon=c_rkNetUpdateActorData.m_dwWeapon;
|
||||
rkNetActorData.m_dwHair=c_rkNetUpdateActorData.m_dwHair;
|
||||
rkNetActorData.m_dwSash=c_rkNetUpdateActorData.m_dwSash; // PORT: AVA2 sash
|
||||
rkNetActorData.m_sAlignment=c_rkNetUpdateActorData.m_sAlignment;
|
||||
rkNetActorData.m_byPKMode=c_rkNetUpdateActorData.m_byPKMode;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ struct SNetworkActorData
|
||||
DWORD m_dwArmor;
|
||||
DWORD m_dwWeapon;
|
||||
DWORD m_dwHair;
|
||||
DWORD m_dwSash; // PORT: AVA2 sash
|
||||
|
||||
DWORD m_dwOwnerVID;
|
||||
|
||||
@@ -85,6 +86,7 @@ struct SNetworkUpdateActorData
|
||||
DWORD m_dwArmor;
|
||||
DWORD m_dwWeapon;
|
||||
DWORD m_dwHair;
|
||||
DWORD m_dwSash; // PORT: AVA2 sash
|
||||
DWORD m_dwMovSpd;
|
||||
DWORD m_dwAtkSpd;
|
||||
short m_sAlignment;
|
||||
@@ -100,6 +102,7 @@ struct SNetworkUpdateActorData
|
||||
m_dwArmor=0;
|
||||
m_dwWeapon=0;
|
||||
m_dwHair=0;
|
||||
m_dwSash=0;
|
||||
m_dwMovSpd=0;
|
||||
m_dwAtkSpd=0;
|
||||
m_sAlignment=0;
|
||||
|
||||
@@ -154,6 +154,7 @@ enum
|
||||
|
||||
HEADER_CG_DRAGON_SOUL_REFINE = 205,
|
||||
HEADER_CG_STATE_CHECKER = 206,
|
||||
HEADER_CG_SASH = 211, // PORT: AVA2 sash (metin2-server FEATURE-AVA2-UNLOCK P4)
|
||||
|
||||
#ifdef __AUCTION__
|
||||
HEADER_CG_AUCTION_CMD = 205,
|
||||
@@ -358,6 +359,7 @@ enum
|
||||
HEADER_GC_SPECIFIC_EFFECT = 208,
|
||||
HEADER_GC_DRAGON_SOUL_REFINE = 209,
|
||||
HEADER_GC_RESPOND_CHANNELSTATUS = 210,
|
||||
HEADER_GC_SASH = 215, // PORT: AVA2 sash
|
||||
|
||||
HEADER_GC_KEY_AGREEMENT_COMPLETED = 0xfa, // _IMPROVED_PACKET_ENCRYPTION_
|
||||
HEADER_GC_KEY_AGREEMENT = 0xfb, // _IMPROVED_PACKET_ENCRYPTION_
|
||||
@@ -1285,6 +1287,7 @@ enum ECharacterEquipmentPart
|
||||
CHR_EQUIPPART_WEAPON,
|
||||
CHR_EQUIPPART_HEAD,
|
||||
CHR_EQUIPPART_HAIR,
|
||||
CHR_EQUIPPART_SASH, // PORT: AVA2 sash, item vnum - 85000
|
||||
|
||||
CHR_EQUIPPART_NUM,
|
||||
};
|
||||
@@ -2401,6 +2404,8 @@ enum SPECIAL_EFFECT
|
||||
SE_EQUIP_HALLOWEEN_CANDY, // 할로윈 사탕을 착용(-_-;)한 순간에 발동하는 이펙트
|
||||
SE_EQUIP_HAPPINESS_RING, // 크리스마스 행복의 반지를 착용하는 순간에 발동하는 이펙트
|
||||
SE_EQUIP_LOVE_PENDANT, // 발렌타인 사랑의 팬던트(71145) 착용할 때 이펙트 (발동이펙트임, 지속이펙트 아님)
|
||||
SE_EFFECT_SASH_SUCCEDED, // PORT: AVA2 sash combine success
|
||||
SE_EFFECT_SASH_EQUIP,
|
||||
};
|
||||
|
||||
typedef struct SPacketGCSpecialEffect
|
||||
@@ -2812,4 +2817,52 @@ typedef struct SChannelStatus
|
||||
BYTE bStatus;
|
||||
} TChannelStatus;
|
||||
|
||||
// PORT: AVA2 sash window. HEADER_CG_SASH and HEADER_GC_SASH share the layout; the server sends the
|
||||
// combine (bWindow true) and absorb windows' state, messages come as "MTSASH <code>" server commands.
|
||||
enum ESashInfo
|
||||
{
|
||||
SASH_ABSORPTION_SOCKET = 0,
|
||||
SASH_ABSORBED_SOCKET = 1,
|
||||
SASH_WINDOW_MAX_MATERIALS = 2,
|
||||
};
|
||||
|
||||
enum
|
||||
{
|
||||
SASH_SUBHEADER_GC_OPEN = 0,
|
||||
SASH_SUBHEADER_GC_CLOSE,
|
||||
SASH_SUBHEADER_GC_ADDED,
|
||||
SASH_SUBHEADER_GC_REMOVED,
|
||||
SASH_SUBHEADER_CG_REFINED,
|
||||
SASH_SUBHEADER_CG_CLOSE = 0,
|
||||
SASH_SUBHEADER_CG_ADD,
|
||||
SASH_SUBHEADER_CG_REMOVE,
|
||||
SASH_SUBHEADER_CG_REFINE,
|
||||
};
|
||||
|
||||
typedef struct SPacketSash
|
||||
{
|
||||
BYTE header;
|
||||
BYTE subheader;
|
||||
bool bWindow;
|
||||
DWORD dwPrice;
|
||||
BYTE bPos;
|
||||
TItemPos tPos;
|
||||
DWORD dwItemVnum;
|
||||
DWORD dwMinAbs;
|
||||
DWORD dwMaxAbs;
|
||||
} TPacketSash;
|
||||
|
||||
typedef struct SSashMaterial
|
||||
{
|
||||
BYTE bHere;
|
||||
WORD wCell;
|
||||
} TSashMaterial;
|
||||
|
||||
typedef struct SSashResult
|
||||
{
|
||||
DWORD dwItemVnum;
|
||||
DWORD dwMinAbs;
|
||||
DWORD dwMaxAbs;
|
||||
} TSashResult;
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
@@ -840,5 +840,8 @@ void initchrmgr()
|
||||
PyModule_AddIntConstant(poModule, "EFFECT_HALLOWEEN_CANDY_EQUIP", CInstanceBase::EFFECT_HALLOWEEN_CANDY_EQUIP);
|
||||
PyModule_AddIntConstant(poModule, "EFFECT_HAPPINESS_RING_EQUIP", CInstanceBase::EFFECT_HAPPINESS_RING_EQUIP);
|
||||
PyModule_AddIntConstant(poModule, "EFFECT_LOVE_PENDANT_EQUIP", CInstanceBase::EFFECT_LOVE_PENDANT_EQUIP);
|
||||
PyModule_AddIntConstant(poModule, "EFFECT_SASH_SUCCEDED", CInstanceBase::EFFECT_SASH_SUCCEDED); // PORT: AVA2 sash
|
||||
PyModule_AddIntConstant(poModule, "EFFECT_SASH_EQUIP", CInstanceBase::EFFECT_SASH_EQUIP);
|
||||
PyModule_AddIntConstant(poModule, "EFFECT_SASH", CInstanceBase::EFFECT_SASH); // refined index, EFFECT_REFINED + EFFECT_SASH
|
||||
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ PyObject * chrCreateInstance(PyObject* poSelf, PyObject* poArgs)
|
||||
kCreateData.m_dwArmor=8;
|
||||
kCreateData.m_dwWeapon=0;
|
||||
kCreateData.m_dwHair=0;
|
||||
kCreateData.m_dwSash=0; // PORT: AVA2 sash
|
||||
kCreateData.m_isMain=false;
|
||||
|
||||
PyObject* poHorse=PyDict_GetItemString(poDict, "horse");
|
||||
@@ -533,6 +534,21 @@ PyObject * chrRefresh(PyObject* poSelf, PyObject* poArgs)
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
// PORT: AVA2 PythonCharacterModule.cpp chrSetSash (ENABLE_SASH_SYSTEM)
|
||||
PyObject * chrSetSash(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
int dwSash;
|
||||
if (!PyTuple_GetInteger(poArgs, 0, &dwSash))
|
||||
return Py_BuildException();
|
||||
|
||||
CInstanceBase * pkInst = CPythonCharacterManager::Instance().GetSelectedInstancePtr();
|
||||
if (!pkInst)
|
||||
return Py_BuildNone();
|
||||
|
||||
pkInst->SetSash(dwSash);
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject * chrMountHorse(PyObject* poSelf, PyObject* poArgs)
|
||||
{
|
||||
CInstanceBase * pkInst = CPythonCharacterManager::Instance().GetSelectedInstancePtr();
|
||||
@@ -1211,6 +1227,7 @@ PyObject * chrtestSetRideMan(PyObject* poSelf, PyObject* poArgs)
|
||||
kCreateData.m_dwRace = 0;
|
||||
kCreateData.m_dwArmor = 0;
|
||||
kCreateData.m_dwHair = 100;
|
||||
kCreateData.m_dwSash = 0; // PORT: AVA2 sash
|
||||
kCreateData.m_dwMovSpd = 100;
|
||||
kCreateData.m_dwAtkSpd = 100;
|
||||
kCreateData.m_dwMountVnum = imount;
|
||||
@@ -1227,6 +1244,7 @@ void initchr()
|
||||
{
|
||||
{ "DismountHorse", chrDismountHorse, METH_VARARGS },
|
||||
{ "MountHorse", chrMountHorse, METH_VARARGS },
|
||||
{ "SetSash", chrSetSash, METH_VARARGS }, // PORT: AVA2 sash
|
||||
|
||||
{ "Destroy", chrDestroy, METH_VARARGS },
|
||||
{ "Update", chrUpdate, METH_VARARGS },
|
||||
@@ -1438,6 +1456,7 @@ void initchr()
|
||||
PyModule_AddIntConstant(poModule, "PART_WEAPON", CRaceData::PART_WEAPON);
|
||||
PyModule_AddIntConstant(poModule, "PART_HEAD", CRaceData::PART_HEAD);
|
||||
PyModule_AddIntConstant(poModule, "PART_WEAPON_LEFT", CRaceData::PART_WEAPON_LEFT);
|
||||
PyModule_AddIntConstant(poModule, "PART_SASH", CRaceData::PART_SASH); // PORT: AVA2 sash
|
||||
|
||||
/////
|
||||
|
||||
|
||||
@@ -640,6 +640,12 @@ void initItem()
|
||||
PyModule_AddIntConstant(poModule, "COSTUME_SLOT_BODY", c_Costume_Slot_Body);
|
||||
PyModule_AddIntConstant(poModule, "COSTUME_SLOT_HAIR", c_Costume_Slot_Hair);
|
||||
PyModule_AddIntConstant(poModule, "COSTUME_SLOT_END", c_Costume_Slot_End);
|
||||
PyModule_AddIntConstant(poModule, "COSTUME_TYPE_SASH", CItemData::COSTUME_SASH); // PORT: AVA2 sash
|
||||
PyModule_AddIntConstant(poModule, "COSTUME_SLOT_SASH", c_Costume_Slot_Sash);
|
||||
PyModule_AddIntConstant(poModule, "COSTUME_TYPE_MOUNT", CItemData::COSTUME_MOUNT); // PORT: AVA2 mount costume
|
||||
PyModule_AddIntConstant(poModule, "COSTUME_TYPE_MOUNT_SKIN", CItemData::COSTUME_MOUNT_SKIN);
|
||||
PyModule_AddIntConstant(poModule, "COSTUME_SLOT_MOUNT", c_Costume_Slot_Mount);
|
||||
PyModule_AddIntConstant(poModule, "COSTUME_SLOT_MOUNT_SKIN", c_Costume_Slot_MountSkin);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_NEW_EQUIPMENT_SYSTEM
|
||||
@@ -754,6 +760,11 @@ void initItem()
|
||||
PyModule_AddIntConstant(poModule, "ARMOR_FOOTS", CItemData::ARMOR_FOOTS);
|
||||
PyModule_AddIntConstant(poModule, "ARMOR_NECK", CItemData::ARMOR_NECK);
|
||||
PyModule_AddIntConstant(poModule, "ARMOR_EAR", CItemData::ARMOR_EAR);
|
||||
PyModule_AddIntConstant(poModule, "ARMOR_PENDANT", CItemData::ARMOR_PENDANT); // PORT: AVA2 talisman
|
||||
PyModule_AddIntConstant(poModule, "EQUIPMENT_PENDANT", c_Equipment_Pendant);
|
||||
PyModule_AddIntConstant(poModule, "ITEM_TYPE_RINGS", CItemData::ITEM_TYPE_RINGS); // PORT: AVA2 new rings
|
||||
PyModule_AddIntConstant(poModule, "EQUIPMENT_RINGS1", c_Equipment_Rings1);
|
||||
PyModule_AddIntConstant(poModule, "EQUIPMENT_RINGS2", c_Equipment_Rings2);
|
||||
|
||||
PyModule_AddIntConstant(poModule, "ITEM_APPLY_MAX_NUM", CItemData::ITEM_APPLY_MAX_NUM);
|
||||
PyModule_AddIntConstant(poModule, "ITEM_SOCKET_MAX_NUM", CItemData::ITEM_SOCKET_MAX_NUM);
|
||||
@@ -852,4 +863,12 @@ void initItem()
|
||||
PyModule_AddIntConstant(poModule, "APPLY_RESIST_DARK", CItemData::APPLY_RESIST_DARK );
|
||||
PyModule_AddIntConstant(poModule, "APPLY_ANTI_CRITICAL_PCT", CItemData::APPLY_ANTI_CRITICAL_PCT );
|
||||
PyModule_AddIntConstant(poModule, "APPLY_ANTI_PENETRATE_PCT", CItemData::APPLY_ANTI_PENETRATE_PCT );
|
||||
PyModule_AddIntConstant(poModule, "APPLY_ATTBONUS_STONE", CItemData::APPLY_ATTBONUS_STONE );
|
||||
PyModule_AddIntConstant(poModule, "APPLY_ATTBONUS_BOSS", CItemData::APPLY_ATTBONUS_BOSS );
|
||||
PyModule_AddIntConstant(poModule, "APPLY_ENCHANT_ELEC", CItemData::APPLY_ENCHANT_ELEC );
|
||||
PyModule_AddIntConstant(poModule, "APPLY_ENCHANT_FIRE", CItemData::APPLY_ENCHANT_FIRE );
|
||||
PyModule_AddIntConstant(poModule, "APPLY_ENCHANT_ICE", CItemData::APPLY_ENCHANT_ICE );
|
||||
PyModule_AddIntConstant(poModule, "APPLY_ENCHANT_WIND", CItemData::APPLY_ENCHANT_WIND );
|
||||
PyModule_AddIntConstant(poModule, "APPLY_ENCHANT_EARTH", CItemData::APPLY_ENCHANT_EARTH );
|
||||
PyModule_AddIntConstant(poModule, "APPLY_ENCHANT_DARK", CItemData::APPLY_ENCHANT_DARK );
|
||||
}
|
||||
|
||||
@@ -184,6 +184,7 @@ class CMainPacketHeaderMap : public CNetworkPacketHeaderMap
|
||||
Set(HEADER_GC_HYBRIDCRYPT_SDB, CNetworkPacketHeaderMap::TPacketType(sizeof(TPacketGCHybridSDB), DYNAMIC_SIZE_PACKET));
|
||||
Set(HEADER_GC_SPECIFIC_EFFECT, CNetworkPacketHeaderMap::TPacketType(sizeof(TPacketGCSpecificEffect), STATIC_SIZE_PACKET));
|
||||
Set(HEADER_GC_DRAGON_SOUL_REFINE, CNetworkPacketHeaderMap::TPacketType(sizeof(TPacketGCDragonSoulRefine), STATIC_SIZE_PACKET));
|
||||
Set(HEADER_GC_SASH, CNetworkPacketHeaderMap::TPacketType(sizeof(TPacketSash), STATIC_SIZE_PACKET)); // PORT: AVA2 sash
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@@ -267,6 +267,12 @@ class CPythonNetworkStream : public CNetworkStream, public CSingleton<CPythonNet
|
||||
// 용홍석 강화
|
||||
bool SendDragonSoulRefinePacket(BYTE bRefineType, TItemPos* pos);
|
||||
|
||||
// PORT: AVA2 sash
|
||||
bool SendSashClosePacket();
|
||||
bool SendSashAddPacket(TItemPos tPos, BYTE bPos);
|
||||
bool SendSashRemovePacket(BYTE bPos);
|
||||
bool SendSashRefinePacket();
|
||||
|
||||
// Handshake
|
||||
bool RecvHandshakePacket();
|
||||
bool RecvHandshakeOKPacket();
|
||||
@@ -545,6 +551,7 @@ class CPythonNetworkStream : public CNetworkStream, public CSingleton<CPythonNet
|
||||
|
||||
// 용혼석 관련
|
||||
bool RecvDragonSoulRefine();
|
||||
bool RecvSashPacket(bool bReturn = false); // PORT: AVA2 sash
|
||||
|
||||
// MiniMap Info
|
||||
bool RecvNPCList();
|
||||
|
||||
@@ -630,6 +630,10 @@ void CPythonNetworkStream::GamePhase()
|
||||
ret = RecvDragonSoulRefine();
|
||||
break;
|
||||
|
||||
case HEADER_GC_SASH: // PORT: AVA2 sash
|
||||
ret = RecvSashPacket();
|
||||
break;
|
||||
|
||||
default:
|
||||
ret = RecvDefaultPacket(header);
|
||||
break;
|
||||
|
||||
@@ -113,6 +113,7 @@ bool CPythonNetworkStream::RecvCharacterAppendPacket()
|
||||
kNetActorData.m_dwArmor=0;/*chrAddPacket.awPart[CHR_EQUIPPART_ARMOR]*/;
|
||||
kNetActorData.m_dwWeapon=0;/*chrAddPacket.awPart[CHR_EQUIPPART_WEAPON]*/;
|
||||
kNetActorData.m_dwHair=0;/*chrAddPacket.awPart[CHR_EQUIPPART_HAIR]*/;
|
||||
kNetActorData.m_dwSash=0; // PORT: AVA2 sash
|
||||
kNetActorData.m_dwMountVnum=0;/*chrAddPacket.dwMountVnum*/;
|
||||
|
||||
kNetActorData.m_dwLevel = 0; // 몬스터 레벨 표시 안함
|
||||
@@ -160,6 +161,7 @@ bool CPythonNetworkStream::RecvCharacterAdditionalInfo()
|
||||
kNetActorData.m_dwArmor=chrInfoPacket.awPart[CHR_EQUIPPART_ARMOR];
|
||||
kNetActorData.m_dwWeapon=chrInfoPacket.awPart[CHR_EQUIPPART_WEAPON];
|
||||
kNetActorData.m_dwHair=chrInfoPacket.awPart[CHR_EQUIPPART_HAIR];
|
||||
kNetActorData.m_dwSash=chrInfoPacket.awPart[CHR_EQUIPPART_SASH]; // PORT: AVA2 sash
|
||||
kNetActorData.m_dwMountVnum=chrInfoPacket.dwMountVnum;
|
||||
|
||||
__RecvCharacterAppendPacket(&kNetActorData);
|
||||
@@ -193,6 +195,7 @@ bool CPythonNetworkStream::RecvCharacterAppendPacketNew()
|
||||
kNetActorData.m_dwArmor=chrAddPacket.awPart[CHR_EQUIPPART_ARMOR];
|
||||
kNetActorData.m_dwWeapon=chrAddPacket.awPart[CHR_EQUIPPART_WEAPON];
|
||||
kNetActorData.m_dwHair=chrAddPacket.awPart[CHR_EQUIPPART_HAIR];
|
||||
kNetActorData.m_dwSash=chrAddPacket.awPart[CHR_EQUIPPART_SASH]; // PORT: AVA2 sash
|
||||
kNetActorData.m_dwStateFlags=chrAddPacket.bStateFlag;
|
||||
kNetActorData.m_dwVID=chrAddPacket.dwVID;
|
||||
kNetActorData.m_dwMountVnum=chrAddPacket.dwMountVnum;
|
||||
@@ -221,6 +224,7 @@ bool CPythonNetworkStream::RecvCharacterUpdatePacket()
|
||||
kNetUpdateActorData.m_dwArmor=chrUpdatePacket.awPart[CHR_EQUIPPART_ARMOR];
|
||||
kNetUpdateActorData.m_dwWeapon=chrUpdatePacket.awPart[CHR_EQUIPPART_WEAPON];
|
||||
kNetUpdateActorData.m_dwHair=chrUpdatePacket.awPart[CHR_EQUIPPART_HAIR];
|
||||
kNetUpdateActorData.m_dwSash=chrUpdatePacket.awPart[CHR_EQUIPPART_SASH]; // PORT: AVA2 sash
|
||||
kNetUpdateActorData.m_dwVID=chrUpdatePacket.dwVID;
|
||||
kNetUpdateActorData.m_kAffectFlags.CopyData(0, sizeof(chrUpdatePacket.dwAffectFlag[0]), &chrUpdatePacket.dwAffectFlag[0]);
|
||||
kNetUpdateActorData.m_kAffectFlags.CopyData(32, sizeof(chrUpdatePacket.dwAffectFlag[1]), &chrUpdatePacket.dwAffectFlag[1]);
|
||||
@@ -246,6 +250,7 @@ bool CPythonNetworkStream::RecvCharacterUpdatePacketNew()
|
||||
kNetUpdateActorData.m_dwArmor=chrUpdatePacket.awPart[CHR_EQUIPPART_ARMOR];
|
||||
kNetUpdateActorData.m_dwWeapon=chrUpdatePacket.awPart[CHR_EQUIPPART_WEAPON];
|
||||
kNetUpdateActorData.m_dwHair=chrUpdatePacket.awPart[CHR_EQUIPPART_HAIR];
|
||||
kNetUpdateActorData.m_dwSash=chrUpdatePacket.awPart[CHR_EQUIPPART_SASH]; // PORT: AVA2 sash
|
||||
kNetUpdateActorData.m_dwVID=chrUpdatePacket.dwVID;
|
||||
kNetUpdateActorData.m_kAffectFlags.CopyData(0, sizeof(chrUpdatePacket.dwAffectFlag[0]), &chrUpdatePacket.dwAffectFlag[0]);
|
||||
kNetUpdateActorData.m_kAffectFlags.CopyData(32, sizeof(chrUpdatePacket.dwAffectFlag[1]), &chrUpdatePacket.dwAffectFlag[1]);
|
||||
|
||||
@@ -858,6 +858,13 @@ bool CPythonNetworkStream::RecvSpecialEffect()
|
||||
case SE_EQUIP_LOVE_PENDANT:
|
||||
effect = CInstanceBase::EFFECT_LOVE_PENDANT_EQUIP;
|
||||
break;
|
||||
// PORT: AVA2 sash
|
||||
case SE_EFFECT_SASH_SUCCEDED:
|
||||
effect = CInstanceBase::EFFECT_SASH_SUCCEDED;
|
||||
break;
|
||||
case SE_EFFECT_SASH_EQUIP:
|
||||
effect = CInstanceBase::EFFECT_SASH_EQUIP;
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#include "StdAfx.h"
|
||||
#include "PythonNetworkStream.h"
|
||||
#include "PythonSash.h"
|
||||
#include "Packet.h"
|
||||
|
||||
// PORT: AVA2 PythonNetworkStreamPhaseGame.cpp sash packets (ENABLE_SASH_SYSTEM) verbatim.
|
||||
bool CPythonNetworkStream::RecvSashPacket(bool bReturn)
|
||||
{
|
||||
TPacketSash sPacket;
|
||||
if (!Recv(sizeof(sPacket), &sPacket))
|
||||
return bReturn;
|
||||
|
||||
bReturn = true;
|
||||
switch (sPacket.subheader)
|
||||
{
|
||||
case SASH_SUBHEADER_GC_OPEN:
|
||||
{
|
||||
CPythonSash::Instance().Clear();
|
||||
PyCallClassMemberFunc(m_apoPhaseWnd[PHASE_WINDOW_GAME], "ActSash", Py_BuildValue("(ib)", 1, sPacket.bWindow));
|
||||
}
|
||||
break;
|
||||
case SASH_SUBHEADER_GC_CLOSE:
|
||||
{
|
||||
CPythonSash::Instance().Clear();
|
||||
PyCallClassMemberFunc(m_apoPhaseWnd[PHASE_WINDOW_GAME], "ActSash", Py_BuildValue("(ib)", 2, sPacket.bWindow));
|
||||
}
|
||||
break;
|
||||
case SASH_SUBHEADER_GC_ADDED:
|
||||
{
|
||||
CPythonSash::Instance().AddMaterial(sPacket.dwPrice, sPacket.bPos, sPacket.tPos);
|
||||
if (sPacket.bPos == 1)
|
||||
{
|
||||
CPythonSash::Instance().AddResult(sPacket.dwItemVnum, sPacket.dwMinAbs, sPacket.dwMaxAbs);
|
||||
PyCallClassMemberFunc(m_apoPhaseWnd[PHASE_WINDOW_GAME], "AlertSash", Py_BuildValue("(b)", sPacket.bWindow));
|
||||
}
|
||||
|
||||
PyCallClassMemberFunc(m_apoPhaseWnd[PHASE_WINDOW_GAME], "ActSash", Py_BuildValue("(ib)", 3, sPacket.bWindow));
|
||||
}
|
||||
break;
|
||||
case SASH_SUBHEADER_GC_REMOVED:
|
||||
{
|
||||
CPythonSash::Instance().RemoveMaterial(sPacket.dwPrice, sPacket.bPos);
|
||||
if (sPacket.bPos == 0)
|
||||
CPythonSash::Instance().RemoveMaterial(sPacket.dwPrice, 1);
|
||||
|
||||
PyCallClassMemberFunc(m_apoPhaseWnd[PHASE_WINDOW_GAME], "ActSash", Py_BuildValue("(ib)", 4, sPacket.bWindow));
|
||||
}
|
||||
break;
|
||||
case SASH_SUBHEADER_CG_REFINED:
|
||||
{
|
||||
if (sPacket.dwMaxAbs == 0)
|
||||
CPythonSash::Instance().RemoveMaterial(sPacket.dwPrice, 1);
|
||||
else
|
||||
{
|
||||
CPythonSash::Instance().RemoveMaterial(sPacket.dwPrice, 0);
|
||||
CPythonSash::Instance().RemoveMaterial(sPacket.dwPrice, 1);
|
||||
}
|
||||
|
||||
PyCallClassMemberFunc(m_apoPhaseWnd[PHASE_WINDOW_GAME], "ActSash", Py_BuildValue("(ib)", 4, sPacket.bWindow));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
TraceError("CPythonNetworkStream::RecvSashPacket: unknown subheader %d\n.", sPacket.subheader);
|
||||
break;
|
||||
}
|
||||
|
||||
return bReturn;
|
||||
}
|
||||
|
||||
bool CPythonNetworkStream::SendSashClosePacket()
|
||||
{
|
||||
if (!__CanActMainInstance())
|
||||
return true;
|
||||
|
||||
TItemPos tPos;
|
||||
tPos.window_type = INVENTORY;
|
||||
tPos.cell = 0;
|
||||
|
||||
TPacketSash sPacket;
|
||||
sPacket.header = HEADER_CG_SASH;
|
||||
sPacket.subheader = SASH_SUBHEADER_CG_CLOSE;
|
||||
sPacket.dwPrice = 0;
|
||||
sPacket.bPos = 0;
|
||||
sPacket.tPos = tPos;
|
||||
sPacket.dwItemVnum = 0;
|
||||
sPacket.dwMinAbs = 0;
|
||||
sPacket.dwMaxAbs = 0;
|
||||
if (!Send(sizeof(sPacket), &sPacket))
|
||||
return false;
|
||||
return SendSequence();
|
||||
}
|
||||
|
||||
bool CPythonNetworkStream::SendSashAddPacket(TItemPos tPos, BYTE bPos)
|
||||
{
|
||||
if (!__CanActMainInstance())
|
||||
return true;
|
||||
|
||||
TPacketSash sPacket;
|
||||
sPacket.header = HEADER_CG_SASH;
|
||||
sPacket.subheader = SASH_SUBHEADER_CG_ADD;
|
||||
sPacket.dwPrice = 0;
|
||||
sPacket.bPos = bPos;
|
||||
sPacket.tPos = tPos;
|
||||
sPacket.dwItemVnum = 0;
|
||||
sPacket.dwMinAbs = 0;
|
||||
sPacket.dwMaxAbs = 0;
|
||||
if (!Send(sizeof(sPacket), &sPacket))
|
||||
return false;
|
||||
|
||||
return SendSequence();
|
||||
}
|
||||
|
||||
bool CPythonNetworkStream::SendSashRemovePacket(BYTE bPos)
|
||||
{
|
||||
if (!__CanActMainInstance())
|
||||
return true;
|
||||
|
||||
TItemPos tPos;
|
||||
tPos.window_type = INVENTORY;
|
||||
tPos.cell = 0;
|
||||
|
||||
TPacketSash sPacket;
|
||||
sPacket.header = HEADER_CG_SASH;
|
||||
sPacket.subheader = SASH_SUBHEADER_CG_REMOVE;
|
||||
sPacket.dwPrice = 0;
|
||||
sPacket.bPos = bPos;
|
||||
sPacket.tPos = tPos;
|
||||
sPacket.dwItemVnum = 0;
|
||||
sPacket.dwMinAbs = 0;
|
||||
sPacket.dwMaxAbs = 0;
|
||||
if (!Send(sizeof(sPacket), &sPacket))
|
||||
return false;
|
||||
|
||||
return SendSequence();
|
||||
}
|
||||
|
||||
bool CPythonNetworkStream::SendSashRefinePacket()
|
||||
{
|
||||
if (!__CanActMainInstance())
|
||||
return true;
|
||||
|
||||
TItemPos tPos;
|
||||
tPos.window_type = INVENTORY;
|
||||
tPos.cell = 0;
|
||||
|
||||
TPacketSash sPacket;
|
||||
sPacket.header = HEADER_CG_SASH;
|
||||
sPacket.subheader = SASH_SUBHEADER_CG_REFINE;
|
||||
sPacket.dwPrice = 0;
|
||||
sPacket.bPos = 0;
|
||||
sPacket.tPos = tPos;
|
||||
sPacket.dwItemVnum = 0;
|
||||
sPacket.dwMinAbs = 0;
|
||||
sPacket.dwMaxAbs = 0;
|
||||
if (!Send(sizeof(sPacket), &sPacket))
|
||||
return false;
|
||||
|
||||
return SendSequence();
|
||||
}
|
||||
@@ -422,6 +422,34 @@ void CPythonPlayer::__UpdateBattleStatus()
|
||||
m_playerStatus.SetPoint(POINT_MAX_MAGIC_WEP, m_dwWeaponMaxMagicPower+m_dwWeaponAddPower);
|
||||
m_playerStatus.SetPoint(POINT_MIN_ATK, __GetTotalAtk(m_dwWeaponMinPower, m_dwWeaponAddPower));
|
||||
m_playerStatus.SetPoint(POINT_MAX_ATK, __GetTotalAtk(m_dwWeaponMaxPower, m_dwWeaponAddPower));
|
||||
|
||||
// PORT: AVA2 PythonPlayer.cpp (ENABLE_SASH_SYSTEM): an equipped sash that absorbed a weapon adds
|
||||
// (value3|4 + value5) and magic (value1|2 + value5) x absorption% to the shown attack, as the
|
||||
// server adds it in Item_GetDamage / SetPolyVarForAttack.
|
||||
const TItemData * pSashData = GetItemData(TItemPos(INVENTORY, c_Costume_Slot_Sash));
|
||||
if (pSashData)
|
||||
{
|
||||
long lAbsorbedVnum = pSashData->alSockets[SASH_ABSORBED_SOCKET];
|
||||
CItemData * pAbsorbed;
|
||||
if (lAbsorbedVnum != 0 && CItemManager::Instance().GetItemDataPointer(lAbsorbedVnum, &pAbsorbed))
|
||||
{
|
||||
if (pAbsorbed->GetType() == CItemData::ITEM_TYPE_WEAPON)
|
||||
{
|
||||
double dAbs = static_cast<double>(pSashData->alSockets[SASH_ABSORPTION_SOCKET]) / 100.0;
|
||||
long lMinWep = static_cast<long>((pAbsorbed->GetValue(3) + pAbsorbed->GetValue(5)) * dAbs + 0.5);
|
||||
long lMaxWep = static_cast<long>((pAbsorbed->GetValue(4) + pAbsorbed->GetValue(5)) * dAbs + 0.5);
|
||||
long lMinMag = static_cast<long>((pAbsorbed->GetValue(1) + pAbsorbed->GetValue(5)) * dAbs + 0.5);
|
||||
long lMaxMag = static_cast<long>((pAbsorbed->GetValue(2) + pAbsorbed->GetValue(5)) * dAbs + 0.5);
|
||||
|
||||
m_playerStatus.SetPoint(POINT_MIN_WEP, m_playerStatus.GetPoint(POINT_MIN_WEP) + lMinWep);
|
||||
m_playerStatus.SetPoint(POINT_MAX_WEP, m_playerStatus.GetPoint(POINT_MAX_WEP) + lMaxWep);
|
||||
m_playerStatus.SetPoint(POINT_MIN_MAGIC_WEP, m_playerStatus.GetPoint(POINT_MIN_MAGIC_WEP) + lMinMag);
|
||||
m_playerStatus.SetPoint(POINT_MAX_MAGIC_WEP, m_playerStatus.GetPoint(POINT_MAX_MAGIC_WEP) + lMaxMag);
|
||||
m_playerStatus.SetPoint(POINT_MIN_ATK, m_playerStatus.GetPoint(POINT_MIN_ATK) + lMinWep);
|
||||
m_playerStatus.SetPoint(POINT_MAX_ATK, m_playerStatus.GetPoint(POINT_MAX_ATK) + lMaxWep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CPythonPlayer::SetStatus(DWORD dwType, long lValue)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
#include "StdAfx.h"
|
||||
|
||||
// PORT: AVA2 PythonSash.cpp (ENABLE_SASH_SYSTEM) verbatim; the sash module is driven by mt_sash.
|
||||
|
||||
#include "PythonSash.h"
|
||||
#include "PythonNetworkStream.h"
|
||||
|
||||
CPythonSash::CPythonSash()
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
|
||||
CPythonSash::~CPythonSash()
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
|
||||
void CPythonSash::Clear()
|
||||
{
|
||||
dwPrice = 0;
|
||||
|
||||
ZeroMemory(&m_vSashResult, sizeof(m_vSashResult));
|
||||
|
||||
m_vSashMaterials.clear();
|
||||
m_vSashMaterials.resize(SASH_WINDOW_MAX_MATERIALS);
|
||||
for (BYTE bPos = 0; bPos < m_vSashMaterials.size(); ++bPos)
|
||||
{
|
||||
TSashMaterial tMaterial;
|
||||
tMaterial.bHere = 0;
|
||||
tMaterial.wCell = 0;
|
||||
m_vSashMaterials[bPos] = tMaterial;
|
||||
}
|
||||
}
|
||||
|
||||
void CPythonSash::AddMaterial(DWORD dwRefPrice, BYTE bPos, TItemPos tPos)
|
||||
{
|
||||
if (bPos >= SASH_WINDOW_MAX_MATERIALS)
|
||||
return;
|
||||
|
||||
if (bPos == 0)
|
||||
dwPrice = dwRefPrice;
|
||||
|
||||
TSashMaterial tMaterial;
|
||||
tMaterial.bHere = 1;
|
||||
tMaterial.wCell = tPos.cell;
|
||||
m_vSashMaterials[bPos] = tMaterial;
|
||||
}
|
||||
|
||||
void CPythonSash::AddResult(DWORD dwItemVnum, DWORD dwMinAbs, DWORD dwMaxAbs)
|
||||
{
|
||||
TSashResult tResult;
|
||||
tResult.dwItemVnum = dwItemVnum;
|
||||
tResult.dwMinAbs = dwMinAbs;
|
||||
tResult.dwMaxAbs = dwMaxAbs;
|
||||
m_vSashResult = tResult;
|
||||
}
|
||||
|
||||
void CPythonSash::RemoveMaterial(DWORD dwRefPrice, BYTE bPos)
|
||||
{
|
||||
if (bPos >= SASH_WINDOW_MAX_MATERIALS)
|
||||
return;
|
||||
|
||||
if (bPos == 1)
|
||||
dwPrice = dwRefPrice;
|
||||
|
||||
TSashMaterial tMaterial;
|
||||
tMaterial.bHere = 0;
|
||||
tMaterial.wCell = 0;
|
||||
m_vSashMaterials[bPos] = tMaterial;
|
||||
}
|
||||
|
||||
bool CPythonSash::GetAttachedItem(BYTE bPos, BYTE & bHere, WORD & wCell)
|
||||
{
|
||||
if (bPos >= SASH_WINDOW_MAX_MATERIALS)
|
||||
return false;
|
||||
|
||||
bHere = m_vSashMaterials[bPos].bHere;
|
||||
wCell = m_vSashMaterials[bPos].wCell;
|
||||
return true;
|
||||
}
|
||||
|
||||
void CPythonSash::GetResultItem(DWORD & dwItemVnum, DWORD & dwMinAbs, DWORD & dwMaxAbs)
|
||||
{
|
||||
dwItemVnum = m_vSashResult.dwItemVnum;
|
||||
dwMinAbs = m_vSashResult.dwMinAbs;
|
||||
dwMaxAbs = m_vSashResult.dwMaxAbs;
|
||||
}
|
||||
|
||||
PyObject * SendSashCloseRequest(PyObject * poSelf, PyObject * poArgs)
|
||||
{
|
||||
CPythonNetworkStream & rkNetStream = CPythonNetworkStream::Instance();
|
||||
rkNetStream.SendSashClosePacket();
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject * SendSashAdd(PyObject * poSelf, PyObject * poArgs)
|
||||
{
|
||||
BYTE bPos;
|
||||
TItemPos tPos;
|
||||
if (!PyTuple_GetInteger(poArgs, 0, &tPos.window_type))
|
||||
return Py_BuildException();
|
||||
else if (!PyTuple_GetInteger(poArgs, 1, &tPos.cell))
|
||||
return Py_BuildException();
|
||||
else if (!PyTuple_GetInteger(poArgs, 2, &bPos))
|
||||
return Py_BuildException();
|
||||
|
||||
CPythonNetworkStream & rkNetStream = CPythonNetworkStream::Instance();
|
||||
rkNetStream.SendSashAddPacket(tPos, bPos);
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject * SendSashRemove(PyObject * poSelf, PyObject * poArgs)
|
||||
{
|
||||
BYTE bPos;
|
||||
if (!PyTuple_GetInteger(poArgs, 0, &bPos))
|
||||
return Py_BuildException();
|
||||
|
||||
CPythonNetworkStream & rkNetStream = CPythonNetworkStream::Instance();
|
||||
rkNetStream.SendSashRemovePacket(bPos);
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject * GetSashPrice(PyObject * poSelf, PyObject * poArgs)
|
||||
{
|
||||
return Py_BuildValue("i", CPythonSash::Instance().GetPrice());
|
||||
}
|
||||
|
||||
PyObject * GetSashAttachedItem(PyObject * poSelf, PyObject * poArgs)
|
||||
{
|
||||
BYTE bPos;
|
||||
if (!PyTuple_GetInteger(poArgs, 0, &bPos))
|
||||
return Py_BuildException();
|
||||
|
||||
BYTE bHere;
|
||||
WORD wCell;
|
||||
bool bGet = CPythonSash::Instance().GetAttachedItem(bPos, bHere, wCell);
|
||||
if (!bGet)
|
||||
{
|
||||
bHere = 0;
|
||||
wCell = 0;
|
||||
}
|
||||
|
||||
return Py_BuildValue("ii", bHere, wCell);
|
||||
}
|
||||
|
||||
PyObject * GetSashResultItem(PyObject * poSelf, PyObject * poArgs)
|
||||
{
|
||||
DWORD dwItemVnum, dwMinAbs, dwMaxAbs;
|
||||
CPythonSash::Instance().GetResultItem(dwItemVnum, dwMinAbs, dwMaxAbs);
|
||||
return Py_BuildValue("iii", dwItemVnum, dwMinAbs, dwMaxAbs);
|
||||
}
|
||||
|
||||
PyObject * SendSashRefineRequest(PyObject * poSelf, PyObject * poArgs)
|
||||
{
|
||||
BYTE bHere;
|
||||
WORD wCell;
|
||||
bool bGet = CPythonSash::Instance().GetAttachedItem(1, bHere, wCell);
|
||||
if (bGet)
|
||||
{
|
||||
if (bHere)
|
||||
{
|
||||
CPythonNetworkStream & rkNetStream = CPythonNetworkStream::Instance();
|
||||
rkNetStream.SendSashRefinePacket();
|
||||
}
|
||||
}
|
||||
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
void initSash()
|
||||
{
|
||||
static PyMethodDef functions[] = {
|
||||
{"SendCloseRequest", SendSashCloseRequest, METH_VARARGS},
|
||||
{"Add", SendSashAdd, METH_VARARGS},
|
||||
{"Remove", SendSashRemove, METH_VARARGS},
|
||||
{"GetPrice", GetSashPrice, METH_VARARGS},
|
||||
{"GetAttachedItem", GetSashAttachedItem, METH_VARARGS},
|
||||
{"GetResultItem", GetSashResultItem, METH_VARARGS},
|
||||
{"SendRefineRequest", SendSashRefineRequest, METH_VARARGS},
|
||||
{NULL, NULL, NULL},
|
||||
};
|
||||
|
||||
PyObject* pModule = Py_InitModule("sash", functions);
|
||||
PyModule_AddIntConstant(pModule, "ABSORPTION_SOCKET", SASH_ABSORPTION_SOCKET);
|
||||
PyModule_AddIntConstant(pModule, "ABSORBED_SOCKET", SASH_ABSORBED_SOCKET);
|
||||
PyModule_AddIntConstant(pModule, "WINDOW_MAX_MATERIALS", SASH_WINDOW_MAX_MATERIALS);
|
||||
PyModule_AddIntConstant(pModule, "CLEAN_ATTR_VALUE_FIELD", 0);
|
||||
PyModule_AddIntConstant(pModule, "LIMIT_RANGE", 1000);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
// PORT: AVA2 sash window state (ENABLE_SASH_SYSTEM), fed by HEADER_GC_SASH.
|
||||
#include "Packet.h"
|
||||
|
||||
class CPythonSash : public CSingleton<CPythonSash>
|
||||
{
|
||||
public:
|
||||
DWORD dwPrice;
|
||||
typedef std::vector<TSashMaterial> TSashMaterials;
|
||||
|
||||
public:
|
||||
CPythonSash();
|
||||
virtual ~CPythonSash();
|
||||
|
||||
void Clear();
|
||||
void AddMaterial(DWORD dwRefPrice, BYTE bPos, TItemPos tPos);
|
||||
void AddResult(DWORD dwItemVnum, DWORD dwMinAbs, DWORD dwMaxAbs);
|
||||
void RemoveMaterial(DWORD dwRefPrice, BYTE bPos);
|
||||
DWORD GetPrice() {return dwPrice;}
|
||||
bool GetAttachedItem(BYTE bPos, BYTE & bHere, WORD & wCell);
|
||||
void GetResultItem(DWORD & dwItemVnum, DWORD & dwMinAbs, DWORD & dwMaxAbs);
|
||||
|
||||
protected:
|
||||
TSashResult m_vSashResult;
|
||||
TSashMaterials m_vSashMaterials;
|
||||
};
|
||||
@@ -68,8 +68,10 @@ void initskill();
|
||||
void initBackground();
|
||||
void initMessenger();
|
||||
void initsafebox();
|
||||
void initSash(); // PORT: AVA2 sash
|
||||
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);
|
||||
|
||||
@@ -43,7 +43,7 @@ inline constexpr int ID_MAX_NUM = 30; // Packet.h -> login[31]
|
||||
inline constexpr int PASS_MAX_NUM = 16; // -> passwd[17]
|
||||
inline constexpr int PRIVATE_CODE_LENGTH = 8;
|
||||
inline constexpr int POINT_MAX_NUM = 255; // common/length.h:58
|
||||
inline constexpr int CHR_EQUIPPART_NUM = 4; // ARMOR/WEAPON/HEAD/HAIR
|
||||
inline constexpr int CHR_EQUIPPART_NUM = 5; // ARMOR/WEAPON/HEAD/HAIR + AVA2 SASH (metin2-server P4)
|
||||
inline constexpr int LOGIN_STATUS_MAX_LEN = 8; // -> szStatus[9]
|
||||
inline constexpr int KEY_AGREEMENT_MAX_DATA_LEN = 256;
|
||||
inline constexpr int ITEM_SOCKET_MAX_NUM = 3; // common/item_length.h
|
||||
@@ -645,7 +645,7 @@ struct GCCharacterAdd2 { // packet_add_char2 — name + parts + guild
|
||||
uint32_t mount_vnum;
|
||||
};
|
||||
static_assert(sizeof(GCCharacterAdd2) ==
|
||||
1 + 4 + (CHARACTER_NAME_MAX_LEN + 1) + 4 + 12 + 1 + 2 + 8 + 1 + 1 + 1 + 8 + 1 + 4 + 2 + 1 + 4); // 120
|
||||
1 + 4 + (CHARACTER_NAME_MAX_LEN + 1) + 4 + 12 + 1 + 2 + 2 * CHR_EQUIPPART_NUM + 1 + 1 + 1 + 8 + 1 + 4 + 2 + 1 + 4); // 122
|
||||
|
||||
struct GCCharacterDel { uint8_t header; uint32_t vid; };
|
||||
static_assert(sizeof(GCCharacterDel) == 5);
|
||||
@@ -873,7 +873,7 @@ struct GCCharacterUpdate { // packet_update_char (header 19)
|
||||
uint8_t pk_mode;
|
||||
uint32_t mount_vnum;
|
||||
};
|
||||
static_assert(sizeof(GCCharacterUpdate) == 1 + 4 + 8 + 1 + 1 + 1 + 8 + 4 + 2 + 1 + 4); // 35
|
||||
static_assert(sizeof(GCCharacterUpdate) == 1 + 4 + 2 * CHR_EQUIPPART_NUM + 1 + 1 + 1 + 8 + 4 + 2 + 1 + 4); // 37
|
||||
|
||||
struct GCStun { uint8_t header; uint32_t vid; }; // header 13
|
||||
struct GCDead { uint8_t header; uint32_t vid; }; // header 14
|
||||
@@ -1036,7 +1036,7 @@ struct GCCharAddInfo {
|
||||
uint8_t pk_mode;
|
||||
uint32_t mount_vnum;
|
||||
};
|
||||
static_assert(sizeof(GCCharAddInfo) == 1 + 4 + (CHARACTER_NAME_MAX_LEN + 1) + 8 + 1 + 4 + 4 + 2 + 1 + 4); // 54
|
||||
static_assert(sizeof(GCCharAddInfo) == 1 + 4 + (CHARACTER_NAME_MAX_LEN + 1) + 2 * CHR_EQUIPPART_NUM + 1 + 4 + 4 + 2 + 1 + 4); // 56
|
||||
|
||||
// --- GC skills / quickslots ---
|
||||
struct PlayerSkill3 { // TPlayerSkill (common/tables.h) — time_t is 32-bit on the -m32 server
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// PORT: adds or replaces whole item_proto records (all 156-byte fields); untouched records stay byte-identical.
|
||||
// Row: vnum type sub size anti flags wear immune buy sell refined refine_set magic_pct
|
||||
// limit0 value0 limit1 value1 apply0 value0 apply1 value1 apply2 value2 v0..v5 specular gain_socket \t name
|
||||
#include "GameLib/StdAfx.h"
|
||||
#include "GameLib/ItemData.h"
|
||||
#include "EterBase/lzo.h"
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
using Item=CItemData::TItemTable;
|
||||
static constexpr std::array<DWORD,4> item_key{{173217,72619434,408587239,27973291}};
|
||||
static std::vector<BYTE> read_file(const char* path){
|
||||
std::ifstream stream(path,std::ios::binary);
|
||||
if(!stream)throw std::runtime_error("cannot read proto");
|
||||
return {std::istreambuf_iterator<char>(stream),std::istreambuf_iterator<char>()};
|
||||
}
|
||||
static DWORD word(const std::vector<BYTE>& data,size_t offset){
|
||||
if(offset+4>data.size())throw std::runtime_error("truncated proto");
|
||||
DWORD value;std::memcpy(&value,data.data()+offset,4);return value;
|
||||
}
|
||||
static void put_word(std::vector<BYTE>& data,size_t offset,DWORD value){std::memcpy(data.data()+offset,&value,4);}
|
||||
static void dump(const Item& r){
|
||||
std::printf("%u %u %u %u %u %u %u %u %u %u %u %u %u",r.dwVnum,r.bType,r.bSubType,r.bSize,r.dwAntiFlags,r.dwFlags,r.dwWearFlags,r.dwImmuneFlag,
|
||||
r.dwIBuyItemPrice,r.dwISellItemPrice,r.dwRefinedVnum,r.wRefineSet,r.bAlterToMagicItemPct);
|
||||
for(const auto& limit:r.aLimits)std::printf(" %u %d",limit.bType,int(limit.lValue));
|
||||
for(const auto& apply:r.aApplies)std::printf(" %u %d",apply.bType,int(apply.lValue));
|
||||
for(auto value:r.alValues)std::printf(" %d",int(value));
|
||||
std::printf(" %u %u\t%.*s\n",r.bSpecular,r.bGainSocketPct,int(strnlen(r.szLocaleName,sizeof(r.szLocaleName))),r.szLocaleName);
|
||||
}
|
||||
int main(int argc,char** argv) {
|
||||
const bool dumping=argc==3&&!std::strcmp(argv[1],"--dump");
|
||||
if(argc!=4&&!dumping){std::fprintf(stderr,"usage: item_proto_patch in_proto rows.tsv out_proto | --dump proto\n");return 2;}
|
||||
if(dumping)argv[1]=argv[2];
|
||||
try {
|
||||
static CLZO lzo; auto key=item_key;
|
||||
auto input=read_file(argv[1]);
|
||||
if(input.size()<20 || std::memcmp(input.data(),"MIPX",4) || word(input,4)!=1 || word(input,8)!=sizeof(Item))throw std::runtime_error("item proto stride");
|
||||
CLZObject decoded;
|
||||
if(!CLZO::Instance().Decompress(decoded,input.data()+20,key.data()))throw std::runtime_error("item proto decode");
|
||||
if(decoded.GetSize()!=size_t(word(input,12))*sizeof(Item))throw std::runtime_error("proto count/size mismatch");
|
||||
std::map<DWORD,Item> rows;
|
||||
for(DWORD i=0;i<word(input,12);++i){Item r;std::memcpy(&r,decoded.GetBuffer()+i*sizeof(Item),sizeof(Item));rows[r.dwVnum]=r;}
|
||||
if(dumping){for(const auto& pair:rows)dump(pair.second);return 0;}
|
||||
std::ifstream file(argv[2],std::ios::binary);std::string line;size_t added=0,replaced=0;
|
||||
while(std::getline(file,line)) {
|
||||
if(line.empty())continue;
|
||||
auto tab=line.find('\t',0);std::string numbers=line,name;
|
||||
// the name is the last tab-separated column
|
||||
auto last=line.rfind('\t');if(last==std::string::npos)throw std::runtime_error("item row without name");
|
||||
numbers=line.substr(0,last);name=line.substr(last+1);(void)tab;
|
||||
std::istringstream stream(numbers);std::array<long long,31> f{};
|
||||
for(auto& value:f)if(!(stream>>value))throw std::runtime_error("item row: "+line.substr(0,40));
|
||||
long long extra;if(stream>>extra)throw std::runtime_error("item row has extra columns");
|
||||
if(name.size()>CItemData::ITEM_NAME_MAX_LEN)throw std::runtime_error("item name too long");
|
||||
Item r{};int i=0;
|
||||
r.dwVnum=f[i++];r.bType=f[i++];r.bSubType=f[i++];r.bSize=f[i++];r.dwAntiFlags=f[i++];r.dwFlags=f[i++];
|
||||
r.dwWearFlags=f[i++];r.dwImmuneFlag=f[i++];r.dwIBuyItemPrice=f[i++];r.dwISellItemPrice=f[i++];
|
||||
r.dwRefinedVnum=f[i++];r.wRefineSet=f[i++];r.bAlterToMagicItemPct=f[i++];
|
||||
for(auto& limit:r.aLimits){limit.bType=f[i++];limit.lValue=f[i++];}
|
||||
for(auto& apply:r.aApplies){apply.bType=f[i++];apply.lValue=f[i++];}
|
||||
for(auto& value:r.alValues)value=f[i++];
|
||||
r.bSpecular=f[i++];r.bGainSocketPct=f[i++];
|
||||
std::memcpy(r.szName,name.data(),name.size());std::memcpy(r.szLocaleName,name.data(),name.size());
|
||||
(rows.count(r.dwVnum)?replaced:added)++;rows[r.dwVnum]=r;
|
||||
}
|
||||
std::vector<BYTE> plain;for(const auto& pair:rows){const BYTE* bytes=reinterpret_cast<const BYTE*>(&pair.second);plain.insert(plain.end(),bytes,bytes+sizeof(Item));}
|
||||
CLZObject encoded;if(!CLZO::Instance().CompressEncryptedMemory(encoded,plain.data(),plain.size(),key.data()))throw std::runtime_error("encode");
|
||||
std::vector<BYTE> result(input.begin(),input.begin()+20);put_word(result,12,rows.size());put_word(result,16,encoded.GetSize());
|
||||
result.insert(result.end(),encoded.GetBuffer(),encoded.GetBuffer()+encoded.GetSize());
|
||||
CLZObject verified;if(!CLZO::Instance().Decompress(verified,result.data()+20,key.data()) || verified.GetSize()!=plain.size() || std::memcmp(verified.GetBuffer(),plain.data(),plain.size()))throw std::runtime_error("round-trip");
|
||||
std::ofstream out(argv[3],std::ios::binary|std::ios::trunc);
|
||||
if(!out.write(reinterpret_cast<const char*>(result.data()),result.size()))throw std::runtime_error("cannot write proto");
|
||||
std::printf("items added=%zu replaced=%zu total=%zu; verified round-trip\n",added,replaced,rows.size());return 0;
|
||||
}catch(const std::exception& e){std::fprintf(stderr,"%s\n",e.what());return 1;}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
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()
|
||||
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
|
||||
|
||||
|
||||
NAMES={70043:'夺宝手套',71153:'经验药水',27992:'白珍珠',27987:'珍珠蚌'}
|
||||
INFO=['MTBP1 season 202610 1790805600 1793487600 1790900000 6',
|
||||
'MTBP1 mission 1 1 0 20000 150 0 70043 1 71153 5',
|
||||
'MTBP1 mission 2 2 0 30 30 0 25041 2',
|
||||
'MTBP1 mission 3 3 0 300 300 1 50513 3',
|
||||
'MTBP1 mission 4 4 0 172800 5400 0 71094 2',
|
||||
'MTBP1 mission 5 5 27987 100 3 0 27987 5',
|
||||
'MTBP1 mission 9 9 27992 10 0 0',
|
||||
'MTBP1 final 0 0 27992 2 27993 1',
|
||||
'MTBP1 end']
|
||||
|
||||
|
||||
class BattlePassTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.chat=[];self.sent=[];self.clock=100.
|
||||
names=('app','chat','game','grp','item','net','nonplayer','ui','mt_biolog')
|
||||
mods={n:types.ModuleType(n) for n in names}
|
||||
app=mods['app'];app.GetTime=lambda:self.clock;app.DIK_F6=64
|
||||
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,BINARY_ServerCommand_Run=lambda s,l:'old'))
|
||||
grp=mods['grp'];grp.SetColor=grp.RenderBar=grp.RenderBox=grp.RenderLine=lambda *a:None
|
||||
self.selected=0
|
||||
def select(v):self.selected=v
|
||||
mods['item'].SelectItem=select;mods['item'].GetItemName=lambda:NAMES.get(self.selected,'')
|
||||
mods['net'].SendChatPacket=self.sent.append
|
||||
mods['nonplayer'].GetMonsterName=lambda v:{691:'狼王'}.get(v,'')
|
||||
mods['ui'].Window=Widget
|
||||
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.previous={n:sys.modules.get(n) for n in mods}
|
||||
sys.modules.update(mods)
|
||||
self.ns={'__name__':'mt_battlepass'};exec(script('BattlePassScript.inc'),self.ns)
|
||||
self.game=mods['game'];self.gw=self.game.GameWindow()
|
||||
self.gw.onPressKeyDict={};self.gw.interface=types.SimpleNamespace(wndMiniMap=Widget())
|
||||
self.game.GameWindow.Open(self.gw)
|
||||
self.w=self.ns['g_window'];self.p=self.ns['g_pass']
|
||||
|
||||
def text(self,t):
|
||||
w=Widget();w.text=t;w.shown=True;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 feed(self,*lines):
|
||||
for line in lines:self.assertEqual(self.game.GameWindow.BINARY_ServerCommand_Run(self.gw,line),1)
|
||||
|
||||
def open(self):
|
||||
self.gw.onPressKeyDict[64]();self.assertTrue(self.w.IsShow());self.assertEqual(self.sent[-1],'/mt_bp 1 info')
|
||||
self.feed(*INFO)
|
||||
|
||||
def test_other_commands_pass_through(self):
|
||||
self.assertEqual(self.game.GameWindow.BINARY_ServerCommand_Run(self.gw,'MTVIP1 times 0 0 0 0 0 0 0'),'old')
|
||||
self.assertIn(64,self.gw.onPressKeyDict)
|
||||
|
||||
def test_info_and_rows(self):
|
||||
self.open();rows=self.w.rows
|
||||
self.assertEqual(sorted(self.p.missions),[1,2,3,4,5,9]);self.assertEqual(self.w.Pages(),2)
|
||||
self.assertEqual(rows[0]['name'].text,'击杀怪物');self.assertEqual(rows[0]['value'].text,'150 / 20,000')
|
||||
self.assertEqual(rows[0]['reward'].text,'奖励:夺宝手套 x1,经验药水 x5')
|
||||
self.assertFalse(rows[0]['button'].enabled);self.assertEqual(rows[0]['button'].text,'未完成')
|
||||
self.assertTrue(rows[1]['button'].enabled);self.assertEqual(rows[1]['button'].text,'领取')
|
||||
self.assertEqual(rows[2]['button'].text,'已领取');self.assertFalse(rows[2]['button'].enabled)
|
||||
self.assertEqual(rows[3]['value'].text,'1.5 / 48 小时')
|
||||
self.assertEqual(rows[4]['name'].text,'钓到 珍珠蚌')
|
||||
self.assertIn('2026 年 10 月',self.w.season.text);self.assertIn('剩余 29 天 22 小时',self.w.season.text)
|
||||
self.w.next.event();self.assertEqual(self.w.page,1)
|
||||
self.assertEqual(rows[0]['name'].text,'向 NPC 出售 白珍珠');self.assertEqual(rows[0]['reward'].text,'无奖励')
|
||||
self.assertFalse(rows[0]['button'].enabled);self.assertFalse(rows[1]['name'].shown)
|
||||
self.assertFalse(self.w.finalButton.enabled);self.assertEqual(self.w.finalState.text,'完成全部任务后可领取')
|
||||
|
||||
def test_progress_done_and_claim(self):
|
||||
self.open()
|
||||
self.feed('MTBP1 progress 1 151');self.assertEqual(self.w.rows[0]['value'].text,'151 / 20,000')
|
||||
self.feed('MTBP1 done 1');self.assertEqual(self.chat[-1],'战令任务完成:击杀怪物,按 F6 领取奖励')
|
||||
self.assertTrue(self.w.rows[0]['button'].enabled)
|
||||
self.clock+=1;self.w.rows[0]['button'].event();self.assertEqual(self.sent[-1],'/mt_bp 1 claim 1')
|
||||
self.assertEqual(self.w.rows[0]['button'].text,'领取中');self.assertFalse(self.w.rows[1]['button'].enabled)
|
||||
self.w.rows[1]['button'].event();self.assertEqual(self.sent[-1],'/mt_bp 1 claim 1') # one claim at a time
|
||||
self.feed('MTBP1 claimed 1');self.assertEqual(self.chat[-1],'战令奖励已放入背包')
|
||||
self.assertEqual(self.w.rows[0]['button'].text,'已领取')
|
||||
self.feed(*INFO) # the server sends the full state after a claim
|
||||
self.clock+=1;self.w.rows[1]['button'].event();self.feed('MTBP1 error space')
|
||||
self.assertEqual(self.chat[-1],'背包空间不足,请整理后再领取');self.assertIn('背包空间不足',self.w.note.text)
|
||||
self.assertTrue(self.w.rows[1]['button'].enabled)
|
||||
|
||||
def test_final(self):
|
||||
self.open()
|
||||
for slot in (1,4,5,9):self.feed('MTBP1 done %d'%slot)
|
||||
self.assertTrue(self.p.AllDone());self.w.Refresh()
|
||||
self.assertTrue(self.w.finalButton.enabled);self.assertEqual(self.w.finalState.text,'全部任务完成')
|
||||
self.clock+=1;self.w.finalButton.event();self.assertEqual(self.sent[-1],'/mt_bp 1 claim 0')
|
||||
self.feed('MTBP1 claimed 0');self.assertEqual(self.chat[-1],'战令最终奖励已放入背包')
|
||||
self.assertEqual(self.w.finalButton.text,'已领取');self.assertFalse(self.w.finalButton.enabled)
|
||||
|
||||
def test_close_none_and_timeout(self):
|
||||
self.open();self.w.OnPressEscapeKey();self.assertEqual(self.sent[-1],'/mt_bp 1 close');self.assertFalse(self.w.IsShow())
|
||||
self.feed('MTBP1 done 2');self.assertEqual(self.chat[-1],'战令任务完成:击杀 Boss,按 F6 领取奖励')
|
||||
self.clock+=1;self.w.Open();self.feed('MTBP1 none')
|
||||
self.assertIsNone(self.p.season);self.assertEqual(self.w.note.text,'当前没有进行中的战令赛季')
|
||||
self.assertFalse(self.w.rows[0]['name'].shown);self.assertFalse(self.w.finalButton.shown)
|
||||
self.w.Hide();self.assertEqual(self.sent[-1],'/mt_bp 1 info') # no season, nothing to close
|
||||
self.clock+=1;self.w.Open();self.clock+=3.5;self.w.OnUpdate()
|
||||
self.assertTrue(self.p.unsupported);self.assertEqual(self.w.note.text,'!服务器尚未启用战令')
|
||||
|
||||
def test_bad_lines_are_dropped(self):
|
||||
self.feed('MTBP1 season 1 2 3 4 0','MTBP1 end');self.assertIsNone(self.p.season)
|
||||
self.feed(INFO[0],INFO[1],'MTBP1 end');self.assertIsNone(self.p.season) # count mismatch
|
||||
self.feed(INFO[0],'MTBP1 mission 1 1 0 x 0 0',*INFO[2:]);self.assertIsNone(self.p.season)
|
||||
self.feed('MTBP1 mission 1 1 0 5 0 0','MTBP1 end');self.assertIsNone(self.p.season) # no season line
|
||||
self.feed('MTBP1 progress 99 1','MTBP1 error nope','MTBP1 bogus')
|
||||
self.feed(*INFO);self.assertEqual(self.p.season[0],202610)
|
||||
self.assertEqual(self.ns['Grouped'](50000000),'50,000,000');self.assertEqual(self.ns['Amount'](4,45,90),'0.8')
|
||||
self.assertEqual(self.ns['Unit'](4,90),' 分钟');self.assertEqual(self.ns['Amount'](4,7200,172800),'2');self.assertEqual(self.ns['SeasonName'](7),'第 7 期')
|
||||
|
||||
def test_registered(self):
|
||||
boot=(ROOT/'src/platform/ScriptLib/PythonBoot.cpp').read_text()
|
||||
self.assertIn('#include "BattlePassScript.inc"',boot)
|
||||
self.assertLess(boot.index('install_script_module("mt_preview"'),boot.index('install_script_module("mt_battlepass"'))
|
||||
self.game.GameWindow.Close(self.gw);self.assertIsNone(self.ns['g_window'])
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -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()
|
||||
@@ -41,7 +41,7 @@ class SwitchbotTests(unittest.TestCase):
|
||||
mods['net'].SendChatPacket=self.sent.append
|
||||
mods['game'].GameWindow=type('GameWindow',(),dict(Open=lambda s:None,Close=lambda s:None,BINARY_ServerCommand_Run=lambda s,line:7))
|
||||
grp=mods['grp'];grp.SetColor=grp.RenderBar=grp.RenderBox=lambda *a:None
|
||||
it=mods['item'];it.ITEM_TYPE_WEAPON=1;it.ITEM_TYPE_ARMOR=2;it.ITEM_TYPE_USE=3;self.selected=None
|
||||
it=mods['item'];it.ITEM_TYPE_WEAPON=1;it.ITEM_TYPE_ARMOR=2;it.ITEM_TYPE_USE=3;it.ARMOR_PENDANT=7;self.selected=None
|
||||
it.SelectItem=lambda v:setattr(self,'selected',v);it.GetItemType=lambda:protos[self.selected][0];it.GetItemSubType=lambda:protos[self.selected][1]
|
||||
it.GetItemName=lambda:'N%d'%self.selected
|
||||
pl=mods['player'];pl.INVENTORY=1;pl.INVENTORY_PAGE_SIZE=45;pl.INVENTORY_PAGE_COUNT=5
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install items-server.tsv (from make_items.py) into share/conf/item_proto.txt + item_names.txt.
|
||||
Rows with the same vnum are replaced, so a rerun is idempotent. The db reads the tables only at boot:
|
||||
restart db and every core afterwards. mobs-server.tsv (optional 4th arg) goes into mob_proto.txt + mob_names.txt
|
||||
the same way. Usage: install_items.py <server root> <items-server.tsv> <fresh backup dir> [<mobs-server.tsv>]
|
||||
"""
|
||||
import shutil,sys
|
||||
from pathlib import Path
|
||||
server,rows,backup=map(Path,sys.argv[1:4])
|
||||
mobrows=Path(sys.argv[4]) if len(sys.argv)>4 else None
|
||||
if backup.exists():raise SystemExit('use a fresh backup directory')
|
||||
load=lambda p:{int(l.split(b'\t',1)[0]):l for l in p.read_bytes().splitlines() if l}
|
||||
backup.mkdir(parents=True)
|
||||
conf=server/'share/conf'
|
||||
|
||||
def install(proto,names,rows):
|
||||
for filename,row in ((proto,lambda v:rows[v]),(names,lambda v:str(v).encode()+b'\t'+rows[v].split(b'\t')[1])):
|
||||
path=conf/filename;raw=path.read_bytes();shutil.copy2(path,backup/filename)
|
||||
newline=b'\r\n' if b'\r\n' in raw else b'\n'
|
||||
kept=[l for l in raw.replace(b'\r\n',b'\n').split(b'\n') if l and not (l.split(b'\t',1)[0].isdigit() and int(l.split(b'\t',1)[0]) in rows)]
|
||||
tmp=path.with_name(filename+'.tmp');tmp.write_bytes(newline.join(kept+[row(v) for v in sorted(rows)])+newline);tmp.replace(path)
|
||||
|
||||
items=load(rows);install('item_proto.txt','item_names.txt',items)
|
||||
print('AVA2 items installed:',len(items),'backup:',backup)
|
||||
if mobrows:
|
||||
mobs=load(mobrows);install('mob_proto.txt','mob_names.txt',mobs)
|
||||
print('AVA2 mobs installed:',len(mobs))
|
||||
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stage the AVA2 drop items that the 203 server lacks as ordinary 40250 items (server rows + client patch pack).
|
||||
Only types/subtypes/flags the 40250 server understands (plus the ported AVA2 systems: sash, mount, costume
|
||||
enchants, talismans, new rings) survive; items of the AVA2-only systems not ported yet (pets, weapon/sash-skin/effect
|
||||
costume enchants, new skill books) become plain ITEM_NONE items. Also writes mobs-server.tsv (MOUNT_MOBS) for install_items.py. Usage: see build_zh_locale.build_ava2_items and --help.
|
||||
"""
|
||||
import argparse,json,re,shutil,subprocess
|
||||
from pathlib import Path
|
||||
HERE=Path(__file__).resolve().parent
|
||||
RANGES=((54001,54027,'时装强化石'),(54028,54078,'发型强化石'),(54079,54123,'武器外观强化石'),
|
||||
(54124,54174,'坐骑外观强化石'),(54175,54219,'披风外观强化石'),(54220,54267,'特效强化石'))
|
||||
# Dragon armor: +0..+9 per class (the refine chain needs every step).
|
||||
DRAGON={21210:'暗龙铠甲',21230:'风龙铠甲',21250:'魔龙铠甲',21270:'神龙铠甲'}
|
||||
# Talismans (P7, server port_pendant.cpp): +0..+200 per element, refine sets 440-499 (../pendant/refine.sql).
|
||||
PENDANT={9600:'火之护符',9830:'冰之护符',10060:'土之护符',10290:'暗之护符',10520:'风之护符',10750:'雷之护符'}
|
||||
PENDANT_STEPS=201
|
||||
# New rings (P8, server port_rings.cpp): +0..+9 per family, 203's refine sets 46-54.
|
||||
RINGS={19500:'红宝石戒指',19520:'石榴石戒指',19540:'祖母绿戒指',19560:'蓝宝石戒指'}
|
||||
# Talisman attribute items: 71302 / 53996 reroll, 71301 / 53997 add (53996 / 53997 are generic on AVA2).
|
||||
PENDANT_SCROLLS={53996,53997,71301,71302}
|
||||
# Models of the armor shapes the 40250 race msm lack (registered by the client's mt_shapes script): shape ->
|
||||
# model stem ('%s' = race); RACE_PATHS = the ShapeData PathName of each race msm.
|
||||
ARMOR_SHAPES={32:'%s_7_1'}
|
||||
RACE_PATHS={'warrior':('pc','pc2'),'assassin':('pc2','pc'),'sura':('pc','pc2'),'shaman':('pc2','pc')} # (male, female)
|
||||
# Kept with their 40250 meaning; everything else becomes ITEM_NONE (see module doc).
|
||||
KEEP_TYPES={'ITEM_MATERIAL','ITEM_QUEST','ITEM_GIFTBOX','ITEM_NONE','ITEM_TREASURE_BOX','ITEM_TREASURE_KEY','ITEM_NEW_RINGS'}
|
||||
KEEP_SUBTYPES={('ITEM_WEAPON',s) for s in ('WEAPON_SWORD','WEAPON_DAGGER','WEAPON_BOW','WEAPON_TWO_HANDED','WEAPON_BELL','WEAPON_FAN')}|{('ITEM_ARMOR','ARMOR_FOOTS'),('ITEM_ARMOR','ARMOR_BODY'),('ITEM_ARMOR','ARMOR_PENDANT'),('ITEM_METIN','METIN_NORMAL')}|{('ITEM_COSTUME',s) for s in ('COSTUME_SASH','COSTUME_MOUNT','COSTUME_MOUNT_SKIN')}
|
||||
DESC={'ITEM_METIN':'可镶嵌到武器的空孔中。','ITEM_MATERIAL':'制作与升级材料。','ITEM_QUEST':'任务物品。','ITEM_GIFTBOX':'打开后可获得物品。','ITEM_TREASURE_BOX':'需要对应的钥匙才能打开。','ITEM_TREASURE_KEY':'可以打开对应的宝箱。','ITEM_WEAPON':'','ITEM_ARMOR':'','ITEM_COSTUME':'披风。可在披风工匠处合成升级,或吸收武器、铠甲的部分属性。',
|
||||
'ITEM_NEW_RINGS':'戒指。装备在两个戒指栏之一。可用钻头开孔(最多 3 孔)并镶嵌对应的宝石,可用转换道具重洗属性、用追加道具增加属性。'}
|
||||
# Sash system (metin2-server docs/FEATURE-AVA2-UNLOCK.md P4): effects registered by the client's mt_sash script
|
||||
# (files they reference are staged too), the window images, and item_scale.txt (model scale per job/sex; a row
|
||||
# covers vnum..vnum+4).
|
||||
SASH_EFFECTS=('d:/ymir work/effect/etc/buff/buff_item6.mse','d:/ymir work/effect/etc/buff/buff_item7.mse',
|
||||
'd:/ymir work/pc/common/effect/armor/acc_01.mse')
|
||||
SASH_LOCALE={'locale/en/ui/acce/acce_combine.tga':'locale/zh/ui/acce/acce_combine.tga',
|
||||
'locale/en/ui/acce/acce_absorb.tga':'locale/zh/ui/acce/acce_absorb.tga'}
|
||||
# Per-subtype descriptions (override DESC).
|
||||
# Costume enchants (P6, server port_costume_attr.cpp): only the body / hair / mount-skin ranges have a 40250
|
||||
# target; AVA2 apply numbers in value0 equal 203's (P2 aligned 139/140). The rest stay locked.
|
||||
ENCHANT_KEEP=((54001,54078),(54124,54174))
|
||||
DESC_SUB={('ITEM_USE','USE_CHANGE_ATTRIBUTE'):'拖到未穿戴的对应时装上,附加一条固定属性(每件时装最多 3 条,不能重复)。',
|
||||
('ITEM_COSTUME','COSTUME_MOUNT'):'坐骑时装。装备后召唤该坐骑(需要已拥有马匹),装备期间属性生效。',
|
||||
('ITEM_COSTUME','COSTUME_MOUNT_SKIN'):'坐骑外观。需先装备坐骑时装,装备后改变坐骑外观。',
|
||||
('ITEM_ARMOR','ARMOR_PENDANT'):'护符。装备在护符栏,对相应属性的怪物伤害提高(强化等级 +1 = +1%)。可用护符强化道具重洗属性,用护符加持道具追加属性(最多 5 条)。'}
|
||||
# Per-vnum descriptions (override DESC_SUB).
|
||||
DESC_VNUM={53996:'拖到未穿戴的护符上,重新随机全部属性。',71302:'拖到未穿戴的护符上,重新随机全部属性。',
|
||||
53997:'拖到未穿戴的护符上,追加一条属性(最多 5 条)。',71301:'拖到未穿戴的护符上,追加一条属性(最多 5 条)。'}
|
||||
# Mount system (P5): races of the mount skins that 203 and the 40250 client lack -> (zh name, npc_mount folder).
|
||||
# Their AVA2 mob_proto rows go to mobs-server.tsv, the whole model folder into the pack; the client's mt_mount
|
||||
# script registers the race names (npclist.txt is read from the root pack only).
|
||||
MOUNT_MOBS={32012:('蓝色巴里','christmas_2017_sleddog'),32028:('白色冬日羊驼','christmas_2020_alpaca')}
|
||||
LOCKED='该物品所属的系统尚未开放,暂时无法使用。'
|
||||
|
||||
def enums(proto_reader):
|
||||
src=proto_reader.read_text(encoding='latin1')
|
||||
def arr(name):
|
||||
m=re.search(r'(?:static\s+)?string\s+'+name+r'\[\]\s*=\s*\{(.*?)\};',src,re.S)
|
||||
return re.findall(r'"([^"]+)"',m.group(1))
|
||||
types=arr('arType');m=re.search(r'arSubType\[\]\s*=\s*\{(.*?)\};',src,re.S)
|
||||
subs=[arr(t) if t!='0' else [] for t in re.findall(r'\b(0|arSub\d+)\b',re.sub(r'//[^\n]*','',m.group(1)))]
|
||||
assert len(subs)==len(types),(len(subs),len(types))
|
||||
return dict(type=types,sub=subs,anti=arr('arAntiFlag'),flag=arr('arFlag'),wear=arr('arWearrFlag'),
|
||||
imm=arr('arImmune'),limit=arr('arLimitType'),apply=arr('arApplyType'))
|
||||
|
||||
def tokens(text):return [t.strip() for t in text.split('|') if t.strip() and t.strip() not in ('NONE','0')]
|
||||
def number(text):
|
||||
try:return int(text)
|
||||
except ValueError:return 0
|
||||
|
||||
def names():
|
||||
result={}
|
||||
for line in (HERE/'names_zh.tsv').read_text().splitlines():
|
||||
if line and not line.startswith('#'):
|
||||
v,n=line.split('\t');result[int(v)]=n
|
||||
for lo,hi,n in RANGES:
|
||||
for v in range(lo,hi+1):result[v]=n
|
||||
for base,n in DRAGON.items():
|
||||
for k in range(10):result[base+k]='%s+%d'%(n,k)
|
||||
for base,n in PENDANT.items():
|
||||
for k in range(PENDANT_STEPS):result[base+k]='%s+%d'%(n,k)
|
||||
for base,n in RINGS.items():
|
||||
for k in range(10):result[base+k]='%s+%d'%(n,k)
|
||||
return result
|
||||
|
||||
def convert(cols,E,imported):
|
||||
"""AVA2 item_proto.txt row (str columns) -> (server row columns, client numeric fields, kept?)."""
|
||||
c=[x.strip() for x in cols[:33]]
|
||||
t,st=c[2],c[3]
|
||||
kept=t in KEEP_TYPES or (t,st) in KEEP_SUBTYPES or ((t,st)==('ITEM_USE','USE_CHANGE_ATTRIBUTE') and
|
||||
any(lo<=int(c[0])<=hi for lo,hi in ENCHANT_KEEP)) or (
|
||||
(t,st) in (('ITEM_USE','USE_CHANGE_ATTRIBUTE'),('ITEM_USE','USE_ADD_ATTRIBUTE')) and int(c[0]) in PENDANT_SCROLLS)
|
||||
anti=tokens(c[5]);flags=tokens(c[6])
|
||||
if 'ITEM_STACKABLE' in anti:anti.remove('ITEM_STACKABLE');flags.append('ITEM_STACKABLE')
|
||||
anti=[a for a in dict.fromkeys(anti) if a in E['anti']];flags=[f for f in dict.fromkeys(flags) if f in E['flag']]
|
||||
if not kept:
|
||||
t,st='ITEM_NONE','0';wear=imm=[];applies=[('APPLY_NONE','0')]*3;values=['0']*6
|
||||
flags=[f for f in flags if f in ('ITEM_STACKABLE','LOG')]
|
||||
else:
|
||||
if t in ('ITEM_QUEST','ITEM_GIFTBOX','ITEM_NONE','ITEM_MATERIAL') and st not in E['sub'][E['type'].index(t)]:st='0'
|
||||
wear=[w for w in tokens(c[7]) if w in E['wear']] if t in ('ITEM_WEAPON','ITEM_ARMOR','ITEM_METIN') else []
|
||||
imm=[i for i in tokens(c[8]) if i in E['imm']]
|
||||
applies=[(a,v) if a in E['apply'] else ('APPLY_NONE','0') for a,v in zip(c[18:24:2],c[19:24:2])]
|
||||
values=[str(number(v)) for v in c[24:30]]
|
||||
refine=(c[11],c[12]) if number(c[11]) in imported else ('0','0')
|
||||
limits=[(l if l in E['limit'] else 'LIMIT_NONE',str(number(v))) for l,v in zip(c[14:18:2],c[15:18:2])]
|
||||
j=lambda xs:' | '.join(xs) if xs else 'NONE'
|
||||
server=[c[0],c[1],t,st,c[4],j(anti),j(flags),j(wear),j(imm),c[9],c[10],refine[0],refine[1],c[13]]
|
||||
for l,v in limits:server+=[l,v]
|
||||
for a,v in applies:server+=[a,str(number(v))]
|
||||
server+=values+[str(number(c[30])),str(number(c[31])),str(number(c[32])) if len(c)>32 and c[32] else '0']
|
||||
bits=lambda xs,k:sum(1<<E[k].index(x) for x in xs)
|
||||
ti=E['type'].index(t);si=E['sub'][ti].index(st) if not st.isdigit() else int(st)
|
||||
client=[int(c[0]),ti,si,number(c[4]),bits(anti,'anti'),bits(flags,'flag'),bits(wear,'wear'),bits(imm,'imm'),
|
||||
number(c[9]),number(c[10]),number(refine[0]),number(refine[1]),number(c[13])]
|
||||
for l,v in limits:client+=[E['limit'].index(l),int(v)]
|
||||
for a,v in applies:client+=[E['apply'].index(a),number(v)]
|
||||
client+=[int(v) for v in values]+[number(c[30]),number(c[31])]
|
||||
return server,client,kept
|
||||
|
||||
def shape_models():
|
||||
"""Pack paths of the ARMOR_SHAPES models (+ LODs) and skins of every race."""
|
||||
out=[]
|
||||
for race,dirs in RACE_PATHS.items():
|
||||
for d in dict.fromkeys(dirs):
|
||||
for stem in ARMOR_SHAPES.values():
|
||||
out+=['d:/ymir work/%s/%s/%s%s'%(d,race,stem%race,e) for e in ('.gr2','.dds','_lod_01.gr2','_lod_02.gr2','_lod_03.gr2')]
|
||||
return out
|
||||
|
||||
def effect_files(files):
|
||||
"""SASH_EFFECTS and every file (mde/textures/models) they name, recursively; names are absolute
|
||||
(d:/ymir work/...) or relative to the referring file."""
|
||||
out=[];todo=list(SASH_EFFECTS)
|
||||
while todo:
|
||||
q=todo.pop()
|
||||
if q in out:continue
|
||||
if q not in files:raise SystemExit('missing AVA2 effect file %s'%q)
|
||||
out.append(q)
|
||||
if q.endswith(('.mse','.mde')):
|
||||
for name in re.findall(rb'[\w \-:./\\]+?\.(?:dds|tga|jpg|mde|gr2|spt|msf)',files[q].read_bytes(),re.I):
|
||||
name=name.decode('latin1').strip().strip('"').lower().replace('\\','/')
|
||||
todo.append(name if name.startswith('d:/') else q.rsplit('/',1)[0]+'/'+name.split('/')[-1])
|
||||
return out
|
||||
|
||||
def pack_index(pack_maker):
|
||||
"""ArchivedPath (lowercase) -> source file, from the AVA2 pack manifests."""
|
||||
index={}
|
||||
for xml in sorted(pack_maker.glob('*_create.xml')):
|
||||
for archived,source in re.findall(r'ArchivedPath="([^"]+)"><!\[CDATA\[([^\]]+)\]\]>',xml.read_text(encoding='latin1')):
|
||||
index.setdefault(archived.lower().replace('\\','/'),pack_maker.parent/source)
|
||||
return index
|
||||
|
||||
def prepare(ava,proto_reader,locale,output,tool,packer,wrap=lambda text:text):
|
||||
"""wrap: the locale's itemdesc line wrapping (build_zh_locale.wrap_description)."""
|
||||
E=enums(proto_reader);zh=names();wanted=set(zh)
|
||||
db=ava/'2. Backup SF+SRC and SQL/home/srv1/db';pack_maker=ava/'1. Client DEV/pack_maker'
|
||||
rows={}
|
||||
for line in (db/'item_proto.txt').read_bytes().splitlines():
|
||||
cols=line.split(b'\t')
|
||||
if cols[0].strip().isdigit() and int(cols[0]) in wanted:rows[int(cols[0])]=[x.decode('latin1') for x in cols]
|
||||
assert rows.keys()==wanted,sorted(wanted-rows.keys())
|
||||
listrows={}
|
||||
for line in (pack_maker/'locale/locale/es/item_list.txt').read_bytes().splitlines():
|
||||
c=[x.strip() for x in line.split(b'\t')]
|
||||
if c and c[0].isdigit() and int(c[0]) in wanted:listrows[int(c[0])]=[x.decode('latin1') for x in c if x]
|
||||
files=pack_index(pack_maker)
|
||||
for v in wanted-listrows.keys(): # 16 items have no AVA2 item_list row; their icons ship under the vnum
|
||||
listrows[v]=[str(v),'ETC','icon/item/%05d.tga'%v]
|
||||
output.mkdir(parents=True,exist_ok=True)
|
||||
assets=output/'assets'
|
||||
if assets.exists():shutil.rmtree(assets)
|
||||
server=[];client=[];report={'kept':[],'locked':[]}
|
||||
for v in sorted(wanted):
|
||||
s,n,kept=convert(rows[v],E,wanted)
|
||||
name=zh[v];s[1]=name
|
||||
server.append('\t'.join(s).encode('gbk'))
|
||||
client.append(('\t'.join(map(str,n))+'\t'+name).encode('gbk'))
|
||||
report['kept' if kept else 'locked'].append(v)
|
||||
copied=0
|
||||
for v,row in listrows.items():
|
||||
for path in row[2:4]:
|
||||
p=path.lower().replace('\\','/')
|
||||
sources=[p]
|
||||
if p.endswith('.gr2'):sources+=[s for s in MODEL_TEXTURES.get(p.rsplit('/',1)[1],())]
|
||||
for q in sources:
|
||||
if q not in files:raise SystemExit('missing AVA2 asset %s for %d'%(q,v))
|
||||
target=assets/q;target.parent.mkdir(parents=True,exist_ok=True)
|
||||
if not target.exists():shutil.copy2(files[q],target);copied+=1
|
||||
mount_files=[q for q in files for _,d in MOUNT_MOBS.values() if q.startswith('d:/ymir work/npc_mount/%s/'%d)]
|
||||
if len({q.split('/')[3] for q in mount_files})!=len(MOUNT_MOBS):raise SystemExit('missing AVA2 mount models')
|
||||
for q in shape_models()+effect_files(files)+mount_files:
|
||||
if q not in files:raise SystemExit('missing AVA2 asset %s'%q)
|
||||
target=assets/q;target.parent.mkdir(parents=True,exist_ok=True)
|
||||
if not target.exists():shutil.copy2(files[q],target);copied+=1
|
||||
(output/'items-server.tsv').write_bytes(b'\n'.join(server)+b'\n')
|
||||
(output/'items-client.tsv').write_bytes(b'\n'.join(client)+b'\n')
|
||||
mobs={}
|
||||
for line in (db/'mob_proto.txt').read_bytes().splitlines():
|
||||
cols=line.split(b'\t')
|
||||
if cols[0].strip().isdigit() and int(cols[0]) in MOUNT_MOBS:
|
||||
v=int(cols[0]);cols[1]=MOUNT_MOBS[v][0].encode('gbk');mobs[v]=b'\t'.join(cols)
|
||||
assert mobs.keys()==MOUNT_MOBS.keys(),sorted(MOUNT_MOBS.keys()-mobs.keys())
|
||||
(output/'mobs-server.tsv').write_bytes(b'\n'.join(mobs[v] for v in sorted(mobs))+b'\n')
|
||||
target=assets/'locale/zh';target.mkdir(parents=True,exist_ok=True)
|
||||
subprocess.run([str(tool),str(locale/'item_proto'),str(output/'items-client.tsv'),str(target/'item_proto')],check=True)
|
||||
for source,dest in SASH_LOCALE.items():
|
||||
(assets/dest).parent.mkdir(parents=True,exist_ok=True);shutil.copy2(files[source],assets/dest);copied+=1
|
||||
scale=[l for l in (pack_maker/'locale/locale/en/item_scale.txt').read_bytes().splitlines()
|
||||
if l[:5].isdigit() and wanted & set(range(int(l[:5]),int(l[:5])+5))]
|
||||
(target/'item_scale.txt').write_bytes(b'\n'.join(scale)+b'\n')
|
||||
for filename in ('item_list.txt','itemdesc.txt'):
|
||||
result=[l for l in (locale/filename).read_bytes().splitlines() if not (l.split(b'\t',1)[0].isdigit() and int(l.split(b'\t',1)[0]) in wanted)]
|
||||
for v in sorted(wanted):
|
||||
if filename=='item_list.txt':result.append('\t'.join(listrows[v]).encode('gbk'))
|
||||
else:
|
||||
t=rows[v][2].strip()
|
||||
desc=DESC_VNUM.get(v) or DESC_SUB.get((t,rows[v][3].strip()),DESC.get(t,'')) if v in report['kept'] else LOCKED
|
||||
result.append(('%d\t%s\t%s'%(v,zh[v],wrap(desc) if desc else '')).encode('gbk'))
|
||||
(target/filename).write_bytes(b'\n'.join(result)+b'\n')
|
||||
subprocess.run([str(packer),str(output/'ava2items'),str(assets),''],check=True)
|
||||
report['assets']=copied
|
||||
(output/'items.json').write_text(json.dumps(report,indent=1)+'\n')
|
||||
return report
|
||||
|
||||
# Textures named inside the (BitKnit-compressed) AVA2 models, read with sdk/gr2.
|
||||
MODEL_TEXTURES={**{'%05d.gr2'%v:('d:/ymir work/item/weapon/weapon_7th_01.dds',) for v in (320,340,1190,2210,3230,5170,7310)},
|
||||
'boss_box.gr2':('d:/ymir work/item/etc/boss_box.dds',),
|
||||
'acce_01_85_004.gr2':('d:/ymir work/item/wing/acce_85_001-008.dds',),'acce_02_85_008.gr2':('d:/ymir work/item/wing/acce_85_001-008.dds',),
|
||||
'acce_03_85_014.gr2':('d:/ymir work/item/wing/acce_85_011-018.dds',),'acce_04_85_018.gr2':('d:/ymir work/item/wing/acce_85_011-018.dds',)}
|
||||
|
||||
if __name__=='__main__':
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
for a in ('ava','proto_reader','locale','output','tool','packer'):p.add_argument(a,type=Path)
|
||||
a=p.parse_args();print(json.dumps({k:len(v) if isinstance(v,list) else v for k,v in prepare(a.ava,a.proto_reader,a.locale,a.output,a.tool.resolve(),a.packer.resolve()).items()}))
|
||||
@@ -0,0 +1,208 @@
|
||||
# AVA2 drop items imported as 40250 items (vnum<TAB>zh name). 54001-54267 are named by range in make_items.py.
|
||||
320 守护者之剑+0
|
||||
340 守护者之刃+0
|
||||
1190 守护者匕首+0
|
||||
2210 守护者之弓+0
|
||||
3230 守护者大刀+0
|
||||
5170 守护者之铃+0
|
||||
6884 稀有度转换器
|
||||
7310 守护者之扇+0
|
||||
9600 火之护符+0
|
||||
9830 冰之护符+0
|
||||
10060 土之护符+0
|
||||
10290 暗之护符+0
|
||||
10520 风之护符+0
|
||||
10750 雷之护符+0
|
||||
15440 炎晶之鞋+0
|
||||
15450 海洋之鞋+0
|
||||
15460 锆石之鞋+0
|
||||
15470 孔雀石之鞋+0
|
||||
19500 红宝石戒指+0
|
||||
19520 石榴石戒指+0
|
||||
19540 祖母绿戒指+0
|
||||
19560 蓝宝石戒指+0
|
||||
25163 缩小的头颅
|
||||
25164 蜘蛛钥匙
|
||||
25165 螺旋钥匙
|
||||
25166 缩小的头颅+
|
||||
25167 拉扎多尔入场券
|
||||
25168 奈梅尔入场券
|
||||
25169 九头蛇护身符
|
||||
28444 破碑之石+4
|
||||
28445 克首领之石+4
|
||||
30400 盖亚宝石
|
||||
30600 图样
|
||||
30601 铠甲设计图
|
||||
30602 蓝色染料
|
||||
30603 红色染料
|
||||
30604 灰色染料
|
||||
30605 金板
|
||||
30606 银板
|
||||
30607 铜板
|
||||
30608 钢板
|
||||
30609 青铜板
|
||||
30610 秘银
|
||||
30611 坚韧绑带
|
||||
30612 结实绳索
|
||||
30614 黑色染料
|
||||
30615 白色染料
|
||||
30616 二氧化钛
|
||||
30617 玛瑙
|
||||
30618 月长石
|
||||
30620 红龙之角
|
||||
30621 红龙鳞片
|
||||
30623 智慧之书
|
||||
30624 魔法宝珠
|
||||
30625 帕德玛刚玉
|
||||
30626 蓝晶石
|
||||
30627 神之果实
|
||||
30628 神圣之花
|
||||
30629 众神灵药
|
||||
30630 龙之翼
|
||||
33031 吊坠宝石
|
||||
39070 灰色碎片
|
||||
39071 白色碎片
|
||||
39072 绿色碎片
|
||||
39073 黄色碎片
|
||||
50275 金色欧凯宝箱
|
||||
50276 银色欧凯宝箱
|
||||
50277 铜色欧凯宝箱
|
||||
50569 秘籍·地震
|
||||
50570 秘籍·星光
|
||||
50571 秘籍·风暴射击
|
||||
50572 秘籍·地狱打击
|
||||
50573 秘籍·致命波动
|
||||
50574 秘籍·流星
|
||||
50575 秘籍·灵气护盾
|
||||
50928 金王战利品
|
||||
50929 银王战利品
|
||||
50930 铜王战利品
|
||||
51110 阿拉多尔宝箱
|
||||
51111 奈梅尔宝箱
|
||||
51112 贝兰塞陶宝箱
|
||||
51115 远古守护者宝箱
|
||||
51116 红龙宝箱
|
||||
51117 远古巨龙宝箱
|
||||
53004 鲁弗斯封印(永久)
|
||||
53996 护符附魔道具
|
||||
53997 护符加持道具
|
||||
54705 九头蛇宝箱
|
||||
54706 65级武器宝箱
|
||||
54707 70级武器宝箱
|
||||
54708 75级武器宝箱
|
||||
54709 防具宝箱
|
||||
54710 装备宝箱
|
||||
54711 饰品宝箱
|
||||
54712 生物学家礼包
|
||||
54713 生物学家袋子
|
||||
54714 生物学家袋子+
|
||||
54715 强化药剂包(小)
|
||||
54716 强化药剂包(中)
|
||||
54717 强化药剂包(特大)
|
||||
55001 宠物蛋白补充剂
|
||||
55004 宠物秘籍·幼年
|
||||
55005 宠物秘籍·野性
|
||||
55006 宠物秘籍·勇敢
|
||||
55009 宠物秘籍宝箱
|
||||
55010 宠物书·石碑
|
||||
55011 宠物书·怪物
|
||||
55012 宠物书·首领
|
||||
55013 宠物书·力量
|
||||
55014 宠物书·智力
|
||||
55015 宠物书·敏捷
|
||||
55016 宠物书·体力
|
||||
55017 宠物书·野兽
|
||||
55018 宠物书·兽人
|
||||
55019 宠物书·秘法
|
||||
55020 宠物书·不死
|
||||
55021 宠物书·恶魔
|
||||
55022 宠物书·经验
|
||||
55023 宠物书·掉宝
|
||||
55024 宠物书·金币
|
||||
55025 宠物书·半兽人
|
||||
55026 宠物书·抗半兽人
|
||||
55027 宠物书·技能伤害
|
||||
55028 宠物书·抗技能伤害
|
||||
55029 宠物书·平均伤害
|
||||
55030 宠物书·抗平均伤害
|
||||
55031 宠物书·抗暴击
|
||||
55033 宠物技能清除剂
|
||||
55035 宠物经验增幅剂
|
||||
55036 宠物栏位解锁器
|
||||
55038 宠物书·抗穿透
|
||||
55039 宠物书·施法速度
|
||||
55040 宠物书·格挡
|
||||
55041 宠物书·抗毒
|
||||
55042 宠物书·冰
|
||||
55043 宠物书·土
|
||||
55044 宠物书·暗
|
||||
55101 活力药剂(小)
|
||||
55102 活力药剂(中)
|
||||
55103 活力药剂(大)
|
||||
55104 活力药剂(特大)
|
||||
55108 战斗药剂(小)
|
||||
55109 战斗药剂(中)
|
||||
55110 战斗药剂(大)
|
||||
55111 战斗药剂(特大)
|
||||
55115 力量药剂(小)
|
||||
55116 力量药剂(中)
|
||||
55117 力量药剂(大)
|
||||
55118 力量药剂(特大)
|
||||
55401 猴子蛋
|
||||
55404 奈梅尔蛋
|
||||
55407 阿兹雷尔蛋
|
||||
71097 炼金附魔道具
|
||||
71234 蓝色巴里
|
||||
71254 白色冬日羊驼
|
||||
71301 护符加持道具
|
||||
71302 护符强化道具
|
||||
71502 狮子封印(永久)
|
||||
71505 野猪封印(永久)
|
||||
71508 狼封印(永久)
|
||||
71511 老虎封印(永久)
|
||||
72064 小型扩充券
|
||||
72065 中型扩充券
|
||||
72066 大型扩充券
|
||||
72067 强效扩充券
|
||||
72068 天界扩充券
|
||||
72319 金钥匙
|
||||
72320 银钥匙
|
||||
72346 6-7属性附魔道具
|
||||
77900 药剂宝箱(小)
|
||||
77901 药剂宝箱(中)
|
||||
77902 药剂宝箱(大)
|
||||
77903 药剂宝箱(特大)
|
||||
77904 阅读宝箱
|
||||
77905 护符宝箱
|
||||
77906 耳环宝箱
|
||||
77907 手镯宝箱
|
||||
77908 项链宝箱
|
||||
77909 戒指宝箱
|
||||
77910 强化宝箱(小)
|
||||
77911 强化宝箱(中)
|
||||
77912 强化宝箱(大)
|
||||
77913 炼金宝箱(小)
|
||||
77914 炼金宝箱(中)
|
||||
77915 炼金宝箱(大)
|
||||
77916 战令宝箱
|
||||
77917 任务书宝箱
|
||||
77918 青铜宝箱
|
||||
77919 白银宝箱
|
||||
77920 黄金宝箱
|
||||
77921 青铜钥匙
|
||||
77922 白银钥匙
|
||||
77923 黄金钥匙
|
||||
77924 钥匙宝箱
|
||||
80019 精致布料
|
||||
80025 时装兑换券
|
||||
80030 男爵夫人的宝箱
|
||||
85004 领主披风(个人)
|
||||
85008 大师披风(个人)
|
||||
85014 王朝披风(个人)
|
||||
85018 皇家披风(个人)
|
||||
96001 火之护符碎片
|
||||
96002 冰之护符碎片
|
||||
96003 土之护符碎片
|
||||
96004 暗之护符碎片
|
||||
96005 风之护符碎片
|
||||
96006 雷之护符碎片
|
||||
|
@@ -0,0 +1,115 @@
|
||||
import sys,unittest
|
||||
from pathlib import Path
|
||||
HERE=Path(__file__).resolve().parents[1];sys.path.insert(0,str(HERE))
|
||||
import make_items as m
|
||||
READER=HERE.parents[3]/'metin2-server/src/server/db/src/ProtoReader.cpp'
|
||||
|
||||
def row(vnum,t,st,anti='NONE',flags='NONE',wear='NONE',applies=('APPLY_NONE','0')*3,values=('0',)*6,refine=('0','0')):
|
||||
return [str(vnum),'x',t,st,'1',anti,flags,wear,'NONE','10','20',refine[0],refine[1],'0','LEVEL','120','LIMIT_NONE','0',*applies,*values,'0','0','-1','']
|
||||
|
||||
@unittest.skipUnless(READER.is_file(),'needs ../metin2-server')
|
||||
class ConvertTests(unittest.TestCase):
|
||||
E=m.enums(READER) if READER.is_file() else None
|
||||
def test_weapon_kept_without_unknown_flags(self):
|
||||
s,c,kept=m.convert(row(320,'ITEM_WEAPON','WEAPON_SWORD','ANTI_MUDANG | ANTI_WOLFMAN','ITEM_TUNABLE','WEAR_WEAPON',
|
||||
('APPLY_ATT_SPEED','18','APPLY_ATTBONUS_BOSS','5','APPLY_NONE','0'),refine=('321','81')),self.E,{320})
|
||||
self.assertTrue(kept);self.assertEqual(s[5],'ANTI_MUDANG');self.assertEqual(s[7],'WEAR_WEAPON')
|
||||
self.assertEqual(s[18:22],['APPLY_ATT_SPEED','18','APPLY_ATTBONUS_BOSS','5']) # ported apply kept
|
||||
self.assertEqual(s[11:13],['0','0']) # the +1 item is not imported
|
||||
self.assertEqual(c[1:3],[self.E['type'].index('ITEM_WEAPON'),0]);self.assertEqual(c[4],1<<self.E['anti'].index('ANTI_MUDANG'))
|
||||
self.assertEqual(len(s),33);self.assertEqual(len(c),31)
|
||||
def test_costume_enchant_kept_only_for_ported_targets(self):
|
||||
for vnum,mask in ((54001,'2'),(54078,'4'),(54130,'16')): # body / hair / mount skin (port_costume_attr.cpp)
|
||||
s,c,kept=m.convert(row(vnum,'ITEM_USE','USE_CHANGE_ATTRIBUTE',flags='ITEM_STACKABLE',values=('23','10',mask,'0','0','0')),self.E,set())
|
||||
self.assertTrue(kept);self.assertEqual(s[2:4],['ITEM_USE','USE_CHANGE_ATTRIBUTE']);self.assertEqual(s[24:27],['23','10',mask])
|
||||
for vnum,mask in ((54100,'8'),(54200,'32'),(54250,'64')): # weapon costume / sash skin / effect
|
||||
s,c,kept=m.convert(row(vnum,'ITEM_USE','USE_CHANGE_ATTRIBUTE',flags='ITEM_STACKABLE',values=('23','10',mask,'0','0','0')),self.E,set())
|
||||
self.assertFalse(kept);self.assertEqual(s[2:4],['ITEM_NONE','0']);self.assertEqual(s[24:30],['0']*6)
|
||||
self.assertEqual(c[1:3],[0,0]);self.assertEqual(c[5],1<<self.E['flag'].index('ITEM_STACKABLE'))
|
||||
def test_unknown_type_and_stackable_anti(self):
|
||||
s,_,kept=m.convert(row(55010,'ITEM_PET','PET_SKILL','ITEM_STACKABLE','LOG','WEAR_PENDANT'),self.E,set())
|
||||
self.assertFalse(kept);self.assertEqual(s[5:8],['NONE','LOG | ITEM_STACKABLE','NONE'])
|
||||
def test_quest_subtype(self):
|
||||
s,c,kept=m.convert(row(30400,'ITEM_QUEST','QUEST_NONE',values=('APPLY_NONE','0','0','0','0','0')),self.E,set())
|
||||
self.assertTrue(kept);self.assertEqual(s[3],'0');self.assertEqual(s[24],'0')
|
||||
def test_treasure_box_and_key_kept(self):
|
||||
for vnum,t in ((77918,'ITEM_TREASURE_BOX'),(77921,'ITEM_TREASURE_KEY')):
|
||||
s,c,kept=m.convert(row(vnum,t,'0',values=('3','0','0','0','0','0')),self.E,set())
|
||||
self.assertTrue(kept);self.assertEqual(s[2:4],[t,'0']);self.assertEqual(s[24],'3') # key value0 == box value0 opens it
|
||||
self.assertEqual(c[1],self.E['type'].index(t))
|
||||
def test_stone_and_boss_stones_kept(self):
|
||||
s,c,kept=m.convert(row(28444,'ITEM_METIN','METIN_NORMAL',flags='ITEM_STACKABLE',wear='WEAR_WEAPON',
|
||||
applies=('APPLY_ATTBONUS_STONE','8','APPLY_NONE','0','APPLY_NONE','0'),values=('0','0','0','0','0','31')),self.E,set())
|
||||
self.assertTrue(kept);self.assertEqual(s[7],'WEAR_WEAPON');self.assertEqual(s[18:20],['APPLY_ATTBONUS_STONE','8'])
|
||||
self.assertEqual(c[17:19],[139,8]) # AVA2 numbering, shared with the server and client enums
|
||||
self.assertEqual(self.E['apply'].index('APPLY_ATTBONUS_BOSS'),140);self.assertEqual(s[29],'31')
|
||||
def test_dragon_armor_kept_with_refine_chain(self):
|
||||
s,c,kept=m.convert(row(21210,'ITEM_ARMOR','ARMOR_BODY','ANTI_ASSASSIN | ANTI_SURA | ANTI_MUDANG | ANTI_WOLFMAN','ITEM_TUNABLE','WEAR_BODY',
|
||||
('APPLY_MOV_SPEED','5','APPLY_RESIST_MAGIC','25','APPLY_MAX_HP_PCT','20'),('0','210','0','32','0','0'),('21211','81')),self.E,{21210,21211})
|
||||
self.assertTrue(kept);self.assertEqual(s[3],'ARMOR_BODY');self.assertEqual(s[7],'WEAR_BODY');self.assertEqual(s[11:13],['21211','81'])
|
||||
self.assertEqual(s[5],'ANTI_ASSASSIN | ANTI_SURA | ANTI_MUDANG');self.assertEqual(s[27],'32') # value3 = armor shape
|
||||
models=m.shape_models();self.assertEqual(len(models),40)
|
||||
self.assertIn('d:/ymir work/pc2/warrior/warrior_7_1_lod_03.gr2',models)
|
||||
def test_sash_kept(self):
|
||||
s,c,kept=m.convert(row(85004,'ITEM_COSTUME','COSTUME_SASH',wear='WEAR_COSTUME_BODY',
|
||||
values=('4','0','0','0','0','0')),self.E,{85004})
|
||||
self.assertTrue(kept);self.assertEqual(s[2:4],['ITEM_COSTUME','COSTUME_SASH']);self.assertEqual(s[24],'4') # value0 = grade
|
||||
self.assertEqual(c[2],3) # COSTUME_SASH, after BODY/HAIR/MOUNT
|
||||
def test_mount_and_skin_kept(self):
|
||||
s,c,kept=m.convert(row(71502,'ITEM_COSTUME','COSTUME_MOUNT','ANTI_STACK',applies=('APPLY_ATTBONUS_MONSTER','30','APPLY_NONE','0','APPLY_NONE','0'),
|
||||
values=('20113','0','0','0','0','0')),self.E,{71502})
|
||||
self.assertTrue(kept);self.assertEqual(s[3],'COSTUME_MOUNT');self.assertEqual(s[18:20],['APPLY_ATTBONUS_MONSTER','30'])
|
||||
self.assertEqual(s[24],'20113');self.assertEqual(c[2],2) # value0 = horse race
|
||||
s,c,kept=m.convert(row(71234,'ITEM_COSTUME','COSTUME_MOUNT_SKIN',values=('32012','0','0','0','0','0')),self.E,{71234})
|
||||
self.assertTrue(kept);self.assertEqual(s[3],'COSTUME_MOUNT_SKIN');self.assertEqual(c[2],4)
|
||||
self.assertEqual(set(m.MOUNT_MOBS),{32012,32028})
|
||||
def test_pendant_and_scrolls_kept(self):
|
||||
s,c,kept=m.convert(row(9601,'ITEM_ARMOR','ARMOR_PENDANT',flags='ITEM_TUNABLE',wear='WEAR_PENDANT',
|
||||
applies=('APPLY_ENCHANT_FIRE','1','APPLY_NONE','0','APPLY_NONE','0'),refine=('9602','441')),self.E,{9601,9602})
|
||||
self.assertTrue(kept);self.assertEqual(s[3],'ARMOR_PENDANT');self.assertEqual(s[7],'NONE') # no WEAR_PENDANT flag: FindEquipCell
|
||||
self.assertEqual(s[11:13],['9602','441']);self.assertEqual(s[18:20],['APPLY_ENCHANT_FIRE','1']);self.assertEqual(c[17],124)
|
||||
self.assertEqual(c[2],self.E['sub'][self.E['type'].index('ITEM_ARMOR')].index('ARMOR_PENDANT'))
|
||||
for vnum,st in ((53996,'USE_CHANGE_ATTRIBUTE'),(71302,'USE_CHANGE_ATTRIBUTE'),(53997,'USE_ADD_ATTRIBUTE'),(71301,'USE_ADD_ATTRIBUTE')):
|
||||
s,c,kept=m.convert(row(vnum,'ITEM_USE',st,flags='ITEM_STACKABLE'),self.E,set())
|
||||
self.assertTrue(kept);self.assertEqual(s[2:4],['ITEM_USE',st])
|
||||
s,c,kept=m.convert(row(71303,'ITEM_USE','USE_ADD_ATTRIBUTE'),self.E,set());self.assertFalse(kept)
|
||||
n=m.names();self.assertEqual([n[v] for v in (9600,9800,10950)],['火之护符+0','火之护符+200','雷之护符+200'])
|
||||
def test_new_rings_kept_with_refine_chain(self):
|
||||
s,c,kept=m.convert(row(19500,'ITEM_NEW_RINGS','0',
|
||||
applies=('APPLY_INT','1','APPLY_RESIST_WARRIOR','1','APPLY_NONE','0'),refine=('19501','46')),self.E,{19500,19501})
|
||||
self.assertTrue(kept);self.assertEqual(s[2:4],['ITEM_NEW_RINGS','0']);self.assertEqual(c[1],36)
|
||||
self.assertEqual(s[11:13],['19501','46']);self.assertEqual(s[18:22],['APPLY_INT','1','APPLY_RESIST_WARRIOR','1'])
|
||||
n=m.names();self.assertEqual([n[v] for v in (19500,19509,19569)],['红宝石戒指+0','红宝石戒指+9','蓝宝石戒指+9'])
|
||||
def test_install_items_with_mobs(self):
|
||||
import subprocess,tempfile
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
d=Path(d);conf=d/'share/conf';conf.mkdir(parents=True)
|
||||
(conf/'item_proto.txt').write_bytes(b'VNUM\tNAME\r\n71502\told\r\n');(conf/'item_names.txt').write_bytes(b'VNUM\tNAME\r\n')
|
||||
(conf/'mob_proto.txt').write_bytes(b'VNUM\tNAME\n20110\tboar\n');(conf/'mob_names.txt').write_bytes(b'20110\tboar\n')
|
||||
(d/'i.tsv').write_bytes(b'71502\tnew\n');(d/'m.tsv').write_bytes(b'32012\tbari\tKING\n')
|
||||
subprocess.run([sys.executable,str(HERE/'install_items.py'),str(d),str(d/'i.tsv'),str(d/'bk'),str(d/'m.tsv')],check=True,capture_output=True)
|
||||
self.assertEqual((conf/'item_proto.txt').read_bytes(),b'VNUM\tNAME\r\n71502\tnew\r\n')
|
||||
self.assertEqual((conf/'mob_proto.txt').read_bytes(),b'VNUM\tNAME\n20110\tboar\n32012\tbari\tKING\n')
|
||||
self.assertEqual((conf/'mob_names.txt').read_bytes(),b'20110\tboar\n32012\tbari\n')
|
||||
self.assertTrue((d/'bk/mob_proto.txt').is_file())
|
||||
def test_sash_effect_closure(self):
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
d=Path(d);files={}
|
||||
def put(name,data=b'x'):
|
||||
f=d/str(len(files));f.write_bytes(data);files[name]=f
|
||||
put(m.SASH_EFFECTS[0],b'MeshFileName "buff_item1.mde"\nTextureFileName "d:/ymir work/effect/a.dds"')
|
||||
for q in m.SASH_EFFECTS[1:]:put(q)
|
||||
put('d:/ymir work/effect/etc/buff/buff_item1.mde',b'"light010.dds"');put('d:/ymir work/effect/a.dds')
|
||||
put('d:/ymir work/effect/etc/buff/light010.dds')
|
||||
got=set(m.effect_files(files))
|
||||
self.assertEqual(got,set(files))
|
||||
del files['d:/ymir work/effect/etc/buff/light010.dds']
|
||||
with self.assertRaises(SystemExit):m.effect_files(files)
|
||||
|
||||
def test_names_cover_ranges(self):
|
||||
n=m.names();self.assertEqual(len(n),514+6*200+4*9);self.assertEqual(n[54100],'武器外观强化石')
|
||||
self.assertEqual([n[v] for v in (21210,21219,21279)],['暗龙铠甲+0','暗龙铠甲+9','神龙铠甲+9'])
|
||||
self.assertTrue(all(len(v.encode('gbk'))<=24 for v in n.values()))
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -0,0 +1,14 @@
|
||||
# Talisman refine sets
|
||||
|
||||
AVA2 talismans (护符, metin2-server docs/FEATURE-AVA2-UNLOCK.md P7) refine +0..+200 through 201 items per element. Every step uses one of ten refine sets per element: fragment ×1 + 30031 ×1 + 33031 ×10, 50000 yang, 100 % (AVA2's own rows).
|
||||
|
||||
| Element | Items | Sets | Fragment |
|
||||
|---|---|---|---|
|
||||
| fire | 9600–9800 | 440–449 | 96001 |
|
||||
| ice | 9830–10030 | 460–469 | 96002 |
|
||||
| earth | 10060–10260 | 480–489 | 96003 |
|
||||
| dark | 10290–10490 | 490–499 | 96004 |
|
||||
| wind | 10520–10720 | 470–479 | 96005 |
|
||||
| elect | 10750–10950 | 450–459 | 96006 |
|
||||
|
||||
`install.sh` (on the server) backs up rows 440–499, replaces them from `refine.sql`, and checks 60 rows. The db loads refine_proto at boot, so restart the db afterwards. The items themselves come from `../ava2-items`.
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
# Installs the talisman refine sets (README.md) into player.refine_proto; the db reads them at boot, so restart the db.
|
||||
set -eu
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
MYSQL=${MYSQL:-mysql -umetin2 -ppassword}
|
||||
BACKUP=/root/backup-pendant/$(date +%Y%m%d-%H%M%S)
|
||||
mkdir -p "$BACKUP"
|
||||
$MYSQL player -e 'SELECT * FROM refine_proto WHERE id BETWEEN 440 AND 499' > "$BACKUP/refine_440_499.tsv"
|
||||
echo "backup: $BACKUP"
|
||||
$MYSQL player < "$HERE/refine.sql"
|
||||
[ "$($MYSQL -N player -e 'SELECT COUNT(*) FROM refine_proto WHERE id BETWEEN 440 AND 499 AND cost=50000')" -eq 60 ] || { echo "refine rows missing" >&2; exit 1; }
|
||||
echo "done; restart the db"
|
||||
@@ -0,0 +1,63 @@
|
||||
-- AVA2 talisman refine sets 440-499 (AVA2 player.refine_proto): element fragment x1 + 30031 x1 + 33031 x10, 50000 yang, 100 %.
|
||||
-- Step n of a talisman chain uses set base + n % 10 (fire 440, elect 450, ice 460, wind 470, earth 480, dark 490).
|
||||
REPLACE INTO refine_proto (id,vnum0,vnum1,vnum2,vnum3,vnum4,count0,count1,count2,count3,count4,cost,src_vnum,result_vnum,prob) VALUES
|
||||
(440,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(441,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(442,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(443,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(444,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(445,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(446,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(447,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(448,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(449,96001,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(450,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(451,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(452,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(453,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(454,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(455,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(456,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(457,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(458,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(459,96006,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(460,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(461,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(462,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(463,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(464,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(465,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(466,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(467,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(468,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(469,96002,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(470,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(471,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(472,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(473,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(474,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(475,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(476,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(477,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(478,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(479,96005,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(480,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(481,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(482,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(483,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(484,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(485,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(486,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(487,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(488,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(489,96003,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(490,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(491,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(492,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(493,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(494,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(495,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(496,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(497,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(498,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100),
|
||||
(499,96004,30031,33031,0,0,1,1,10,0,0,50000,0,0,100);
|
||||
@@ -0,0 +1,12 @@
|
||||
# 披风 (sash) quest
|
||||
|
||||
AVA2 `Globales/sash.lua` ported to 王铁匠 (20016; 203 has no NPC 60003):
|
||||
披风说明 / 披风合成 (`pc.open_sash(true)`) / 属性吸收 (`pc.open_sash(false)`).
|
||||
|
||||
Needs the sash game binary (server `port_sash.cpp`, CG 211 / GC 215) and a client with `mt_sash`.
|
||||
|
||||
sh install.sh # on the server, from this directory; then restart the game cores
|
||||
|
||||
Adds `pc.open_sash` to quest_functions, compiles only sash.quest in an isolated staging dir, copies its
|
||||
objects (state/sash + 20016/chat/sash.start.{0,1,2}.*) into the live tree, and adds it to locale_list.
|
||||
Backups go to /root/backup-sash/<time>.
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
# Installs quest sash (README.md): run on the server from this directory, then restart the game cores.
|
||||
# Adds pc.open_sash to quest_functions (needs the sash game binary; an older core would fail the call, not the boot),
|
||||
# compiles only sash.quest in an isolated staging directory and copies its objects into the live quest tree.
|
||||
set -eu
|
||||
SERVER=${SERVER:-/usr/metin2/server}
|
||||
LOCALE=${LOCALE:-newcibn}
|
||||
QUEST="$SERVER/share/locale/$LOCALE/quest"
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
BACKUP=/root/backup-sash/$(date +%Y%m%d-%H%M%S)
|
||||
export LC_ALL=C
|
||||
|
||||
mkdir -p "$BACKUP/stage"
|
||||
cp -p "$QUEST/quest_functions" "$QUEST/locale_list" "$BACKUP/"
|
||||
[ -f "$QUEST/sash.quest" ] && cp -p "$QUEST/sash.quest" "$BACKUP/"
|
||||
echo "backup: $BACKUP"
|
||||
|
||||
# A GBK trail byte 0x5C would be a backslash to qc.
|
||||
iconv -f UTF-8 -t GBK "$HERE/sash.quest" > "$BACKUP/sash.gbk"
|
||||
if grep -q '\\' "$BACKUP/sash.gbk"; then echo "sash.quest: backslash byte in GBK" >&2; exit 1; fi
|
||||
cp "$QUEST/quest_functions" "$BACKUP/stage/"
|
||||
grep -qx 'pc.open_sash' "$BACKUP/stage/quest_functions" || echo 'pc.open_sash' >> "$BACKUP/stage/quest_functions"
|
||||
cp "$BACKUP/sash.gbk" "$BACKUP/stage/sash.quest"
|
||||
(cd "$BACKUP/stage" && "$QUEST/qc" sash.quest) > "$BACKUP/qc.log" 2>&1 || { cat "$BACKUP/qc.log" >&2; exit 1; }
|
||||
test -s "$BACKUP/stage/object/state/sash"
|
||||
[ "$(cd "$BACKUP/stage/object/20016/chat" && ls | grep -c '^sash\.')" -eq 9 ] || { echo "unexpected qc output" >&2; exit 1; }
|
||||
|
||||
cat "$BACKUP/stage/quest_functions" > "$QUEST/quest_functions"
|
||||
cat "$BACKUP/sash.gbk" > "$QUEST/sash.quest"
|
||||
grep -qx 'sash.quest' "$QUEST/locale_list" || echo 'sash.quest' >> "$QUEST/locale_list"
|
||||
(cd "$BACKUP/stage/object" && find . -type f) | while read -r f; do
|
||||
mkdir -p "$QUEST/object/$(dirname "$f")"
|
||||
cp "$BACKUP/stage/object/$f" "$QUEST/object/$f"
|
||||
done
|
||||
echo "done; restart the game cores"
|
||||
@@ -0,0 +1,50 @@
|
||||
-- 披风 (AVA2 Globales/sash.lua): 合成 / 属性吸收 窗口由 pc.open_sash(true/false) 打开。
|
||||
-- AVA2 用 NPC 60003, 203 没有这个 NPC, 挂在 王铁匠 20016。
|
||||
quest sash begin
|
||||
state start begin
|
||||
when 20016.chat."披风说明" begin
|
||||
say_title(mob_name(20016))
|
||||
say("")
|
||||
say("披风可以吸收武器或防具的属性.")
|
||||
say("")
|
||||
say("[披风合成]")
|
||||
say("放入两件同等级的披风合成, 成功后")
|
||||
say("得到吸收率更高的披风, 失败则材料消失.")
|
||||
say("")
|
||||
say("[属性吸收]")
|
||||
say("放入披风和一件武器或防具, 披风按吸收率")
|
||||
say("获得其属性, 被吸收的装备会消失.")
|
||||
say("")
|
||||
end
|
||||
|
||||
when 20016.chat."披风合成" begin
|
||||
say_title(mob_name(20016))
|
||||
say("")
|
||||
say("要打开披风合成窗口吗?")
|
||||
say("")
|
||||
local confirm = select("确定", "取消")
|
||||
if confirm == 2 then
|
||||
return
|
||||
end
|
||||
|
||||
setskin(NOWINDOW)
|
||||
pc.open_sash(true)
|
||||
end
|
||||
|
||||
when 20016.chat."属性吸收" begin
|
||||
say_title(mob_name(20016))
|
||||
say("")
|
||||
say("被吸收的装备会消失, 披风原有的吸收")
|
||||
say("属性会被替换.")
|
||||
say("要打开属性吸收窗口吗?")
|
||||
say("")
|
||||
local confirm = select("确定", "取消")
|
||||
if confirm == 2 then
|
||||
return
|
||||
end
|
||||
|
||||
setskin(NOWINDOW)
|
||||
pc.open_sash(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user