Add AVA2-style display options (effect level, trees, clouds)
Client-only port of ENABLE_GRAPHIC_ON_OFF / HIDE_OBJECTS: five effect levels toggle attached actor effects via Active/Deactive, tree and cloud parts use background.SetVisiblePart, plus shortcuts for the original name and damage options. mt_display window, mt_display.cfg, port.display test, docs/AVA2-DISPLAY.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e22d7ba6fd
commit
e546f91e03
@@ -35,6 +35,10 @@ __pycache__/
|
||||
# Written by 40250 uitaskbar.py at runtime.
|
||||
/mouse.cfg
|
||||
|
||||
# Written at runtime by the mt_* option windows (mt_pickup, mt_display, ...).
|
||||
/mt_*.cfg
|
||||
/Client/mt_*.cfg
|
||||
|
||||
# native Android client (./android/build.sh): SDL3 download, NDK builds, Gradle output
|
||||
/android/.gradle/
|
||||
/android/app/build/
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# AVA2 显示开关适配进度(v1)
|
||||
|
||||
2026-10-15。只改了 metin2-client,服务端不需要改动。参考了 AVA2 的 `ENABLE_GRAPHIC_ON_OFF`(`InstanceBase.cpp` 中 `__CanRender` 的 effect_level 0–4)和 `HIDE_OBJECTS`(`background.SetVisiblePart`)。
|
||||
|
||||
## 与 AVA2 的差异
|
||||
|
||||
| 项目 | AVA2 | 本实现 |
|
||||
| --- | --- | --- |
|
||||
| 特效等级 | 0–4,在渲染判断里跳过 | 相同的 5 档;在 `CInstanceBase::Update` 中对角色挂载的特效调用 Active/Deactive,不改渲染路径 |
|
||||
| 摆摊、宠物、坐骑的显示范围 | 有 | 不移植:经典版没有离线摆摊和宠物 |
|
||||
| 树木、云 | `background.SetVisiblePart` | 相同,使用原有接口 |
|
||||
| 名字、伤害数字 | 原有选项 | 窗口内加了快捷开关,仍然写原来的 systemSetting,退出时由原配置保存 |
|
||||
|
||||
## 特效等级
|
||||
|
||||
| 等级 | 显示的特效 |
|
||||
| --- | --- |
|
||||
| 0 全部特效 | 全部 |
|
||||
| 1 只看怪物特效 | 自己、怪物、灵石 |
|
||||
| 2 只看玩家特效 | 自己、其他玩家 |
|
||||
| 3 只看自己特效 | 只有自己 |
|
||||
| 4 关闭全部特效 | 无,包括自己 |
|
||||
|
||||
## 客户端
|
||||
|
||||
- C++:
|
||||
- `CEffectManager::ActiveEffectInstance`。
|
||||
- `CActorInstance::SetAttachingEffectShown`:对已挂载的特效调用 Active/Deactive;关闭期间新挂载的特效也会立即 Deactive。使用 Deactive 而不是 Hide,避免和刺客隐身的 Show/Hide 冲突。
|
||||
- `CPythonSystem::Get/SetEffectLevel`,越界值按 0 处理;不写入 metin2.cfg。
|
||||
- `systemSetting.SetEffectLevel/GetEffectLevel`。
|
||||
- `CInstanceBase::__UpdateEffectShown`,每帧在 `Update` 开头按上表判断。
|
||||
- 模块 `mt_display`(`src/platform/ScriptLib/DisplayScript.inc`),在 `PythonBoot.cpp` 中安装:
|
||||
- 入口是小地图旁“拾取”下方的“显示”按钮。
|
||||
- 窗口“显示设置”包含 5 个特效等级、显示树木、显示云、总是显示名字、显示伤害数字,`【】` 表示当前选中。
|
||||
- 设置保存在客户端目录的 `mt_display.cfg`,格式为“特效等级 树木 云”,默认 `0 1 1`,损坏时回到默认值。
|
||||
- 特效等级在启动时生效;树木和云在进入游戏(`GameWindow.Open`)时生效。
|
||||
|
||||
## 证据
|
||||
|
||||
- `tests/wiki/test_display.py`(CTest:`port.display`)共 3 项通过,覆盖:
|
||||
- 默认值、配置文件读写、损坏的配置;
|
||||
- 进入游戏时生效,窗口按钮、配置保存、名字和伤害开关;
|
||||
- 5 档和 C++ 规则一致。
|
||||
- `test_pickup.py`(7 项)和 `test_switchbot.py`(6 项)仍然通过。
|
||||
- 2026-10-15 联机测试(203 测试核心 channel5,macOS 原生客户端,账号 admin):
|
||||
- 切到等级 4 并关闭树木后,配置为 `4 0 1`;恢复后为 `0 1 1`。
|
||||
- 截图确认窗口布局正常。等级 4 的截图里角色被窗口挡住,特效隐藏没有截图对比,只有日志确认等级已设置。
|
||||
|
||||
## 限制和未完成
|
||||
|
||||
- 只控制挂载在角色上的特效(装备光效、buff、种族特效)。单独播放的特效和飞行特效(技能弹道、命中特效)不受影响。
|
||||
- macOS 桌面客户端没有中文字体,窗口文字显示为 `?`(原版界面同样乱码);Android 使用 CFF FreeType,不受影响。
|
||||
- 测试点附近没有树木,树木开关只通过日志和配置确认,没有截图对比。
|
||||
- Android 真机测试未做。
|
||||
@@ -120,3 +120,5 @@
|
||||
2026-10-15 自动洗练 v1(阶段 6,不含属性锁定):服务端 mt_switchbot 逐次调用原 UseItem 洗练路径,客户端 mt_switchbot 窗口;隔离环境编译与桩测试通过,macOS 客户端已在 203 测试核心联机验收;未部署 203 正式环境、未做 Android 真机测试。属性锁定仍待存储设计。见 AVA2-SWITCHBOT.md。
|
||||
|
||||
2026-10-15 批量拾取(计划外追加,参考 AVA2 do_pickup_all):服务端 mt_pickup 对范围内自己的物品按分类逐件调用原 PickupItem,客户端在拾取键(Z)上接入并提供分类设置窗口;macOS 客户端已在 203 测试核心联机验收,未部署 203 正式环境、未做 Android 真机测试和触屏按钮。见 AVA2-PICKUP.md。
|
||||
|
||||
2026-10-15 显示开关(计划外追加,参考 AVA2 ENABLE_GRAPHIC_ON_OFF / HIDE_OBJECTS):仅客户端,5 档特效等级(挂载特效 Active/Deactive)、树木和云开关、名字和伤害数字快捷开关;macOS 客户端已在 203 测试核心联机验收,未做 Android 真机测试。不移植摆摊/宠物/坐骑显示范围。见 AVA2-DISPLAY.md。
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// PORT: AVA2-style display options (ENABLE_GRAPHIC_ON_OFF effect level, HIDE_OBJECTS tree/cloud parts),
|
||||
// client-only. The effect level is systemSetting.SetEffectLevel (CInstanceBase::__UpdateEffectShown deactivates
|
||||
// attached effects); trees and clouds use the original background.SetVisiblePart; names and damage numbers
|
||||
// are shortcuts to the original game options. See docs/AVA2-DISPLAY.md.
|
||||
static const char c_szDisplayScript[] = R"PY(# -*- coding: utf-8 -*-
|
||||
import background
|
||||
import game
|
||||
import grp
|
||||
import systemSetting
|
||||
import ui
|
||||
import weakref
|
||||
from mt_biolog import Action, Text, L
|
||||
from mt_wiki import B
|
||||
|
||||
g_window=None
|
||||
g_entry=None
|
||||
CONFIG='mt_display.cfg'
|
||||
LEVELS=(u'全部特效',u'只看怪物特效',u'只看玩家特效',u'只看自己特效',u'关闭全部特效')
|
||||
LEVEL_HINTS=('显示所有角色的特效','隐藏其他玩家和NPC的特效,保留自己和怪物的','隐藏怪物和NPC的特效,保留玩家的',
|
||||
'只保留自己的特效','连自己的特效也隐藏')
|
||||
|
||||
|
||||
class Options(object):
|
||||
def __init__(self):self.effect=0;self.tree=True;self.cloud=True
|
||||
|
||||
def Load(self,text):
|
||||
words=text.split()
|
||||
if len(words)!=3 or not all(w.isdigit() and len(w)==1 for w in words):return
|
||||
effect,tree,cloud=[int(w) for w in words]
|
||||
if effect<len(LEVELS) and tree in (0,1) and cloud in (0,1):self.effect=effect;self.tree=tree==1;self.cloud=cloud==1
|
||||
|
||||
def Dump(self):return '%d %d %d'%(self.effect,int(self.tree),int(self.cloud))
|
||||
|
||||
def Apply(self):
|
||||
systemSetting.SetEffectLevel(self.effect)
|
||||
background.SetVisiblePart(background.PART_TREE,int(self.tree))
|
||||
background.SetVisiblePart(background.PART_CLOUD,int(self.cloud))
|
||||
|
||||
|
||||
def LoadConfig(options):
|
||||
try:
|
||||
f=open(CONFIG,'r');text=f.read();f.close();options.Load(text)
|
||||
except (IOError,OSError):pass
|
||||
|
||||
|
||||
def SaveConfig(options):
|
||||
try:
|
||||
f=open(CONFIG,'w');f.write(options.Dump());f.close()
|
||||
except (IOError,OSError):pass
|
||||
|
||||
|
||||
def Mark(text,on):return L(B(u'【%s】'%text if on else text))
|
||||
|
||||
|
||||
class DisplayWindow(ui.Window):
|
||||
def __init__(self,options):
|
||||
ui.Window.__init__(self);self.SetWindowName('DisplayWindow');self.AddFlag('float');self.AddFlag('movable')
|
||||
self.options=options;self.SetSize(332,404)
|
||||
self.heading=Text(self,18,18,'显示设置',0xffffd68b)
|
||||
self.closeButton=Action(self,256,6,60,'关闭',self.Hide)
|
||||
self.effectText=Text(self,18,54,'特效',0xffc3b7a1)
|
||||
self.levelButtons=[Action(self,18+(i%2)*152,76+(i//2)*52,144,'',lambda level=i:self.SetLevel(level)) for i in range(len(LEVELS))]
|
||||
self.sceneText=Text(self,18,238,'场景和文字',0xffc3b7a1)
|
||||
self.treeButton=Action(self,18,260,144,'',self.ToggleTree)
|
||||
self.cloudButton=Action(self,170,260,144,'',self.ToggleCloud)
|
||||
self.nameButton=Action(self,18,312,144,'',self.ToggleName)
|
||||
self.damageButton=Action(self,170,312,144,'',self.ToggleDamage)
|
||||
self.status=Text(self,18,376,'',0xffc3b7a1)
|
||||
self.Refresh();self.Hide()
|
||||
|
||||
def Buttons(self):return [self.closeButton,self.treeButton,self.cloudButton,self.nameButton,self.damageButton]+self.levelButtons
|
||||
|
||||
def Open(self):self.Refresh();self.SetCenterPosition();self.Show();self.SetTop()
|
||||
|
||||
def Changed(self):self.options.Apply();SaveConfig(self.options);self.Refresh()
|
||||
|
||||
def SetLevel(self,level):self.options.effect=level;self.Changed()
|
||||
|
||||
def ToggleTree(self):self.options.tree=not self.options.tree;self.Changed()
|
||||
|
||||
def ToggleCloud(self):self.options.cloud=not self.options.cloud;self.Changed()
|
||||
|
||||
# Names and damage numbers are the original game options (saved by the client config on exit).
|
||||
def ToggleName(self):systemSetting.SetAlwaysShowNameFlag(not systemSetting.IsAlwaysShowName());self.Refresh()
|
||||
|
||||
def ToggleDamage(self):systemSetting.SetShowDamageFlag(not systemSetting.IsShowDamage());self.Refresh()
|
||||
|
||||
def Refresh(self):
|
||||
o=self.options
|
||||
for i,button in enumerate(self.levelButtons):button.SetText(Mark(LEVELS[i],i==o.effect))
|
||||
self.treeButton.SetText(Mark(u'显示树木',o.tree));self.cloudButton.SetText(Mark(u'显示云',o.cloud))
|
||||
self.nameButton.SetText(Mark(u'总是显示名字',systemSetting.IsAlwaysShowName()))
|
||||
self.damageButton.SetText(Mark(u'显示伤害数字',systemSetting.IsShowDamage()))
|
||||
self.status.SetText(L(LEVEL_HINTS[o.effect]))
|
||||
|
||||
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()
|
||||
for b in self.Buttons():b.event=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('DisplayEntry');self.OnUpdate()
|
||||
|
||||
def OnUpdate(self):
|
||||
x,y=self.minimap.GetGlobalPosition();self.SetPosition(max(4,x-68),y+252)
|
||||
|
||||
|
||||
g_options=Options()
|
||||
LoadConfig(g_options)
|
||||
systemSetting.SetEffectLevel(g_options.effect)
|
||||
|
||||
|
||||
def Toggle():
|
||||
if g_window:
|
||||
if g_window.IsShow():g_window.Hide()
|
||||
else:g_window.Open()
|
||||
|
||||
|
||||
_open=game.GameWindow.Open
|
||||
_close=game.GameWindow.Close
|
||||
|
||||
def Open(self):
|
||||
global g_window,g_entry
|
||||
_open(self);g_options.Apply();g_window=DisplayWindow(g_options);g_entry=Entry(self)
|
||||
|
||||
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)
|
||||
|
||||
game.GameWindow.Open=Open
|
||||
game.GameWindow.Close=Close
|
||||
)PY";
|
||||
@@ -1001,6 +1001,7 @@ namespace PythonBoot
|
||||
#include "WikiEntryScript.inc"
|
||||
#include "SwitchbotScript.inc"
|
||||
#include "PickupScript.inc"
|
||||
#include "DisplayScript.inc"
|
||||
|
||||
static void install_script_module(const char* c_szModule, const char* c_szScript)
|
||||
{
|
||||
@@ -1076,6 +1077,7 @@ bool RunMainScript(const char* lpCmdLine, std::string* error)
|
||||
install_script_module("mt_wiki_entries", c_szWikiEntryScript);
|
||||
install_script_module("mt_switchbot", c_szSwitchbotScript);
|
||||
install_script_module("mt_pickup", c_szPickupScript);
|
||||
install_script_module("mt_display", c_szDisplayScript);
|
||||
if (g_touch_input)
|
||||
install_script_module("mt_mobile_hud", c_szMobileHUDScript);
|
||||
}
|
||||
|
||||
@@ -320,6 +320,8 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
|
||||
COMMAND ${MT_WIKI_PYTHON3} ${PROJECT_SOURCE_DIR}/tests/wiki/test_switchbot.py)
|
||||
add_test(NAME port.pickup
|
||||
COMMAND ${MT_WIKI_PYTHON3} ${PROJECT_SOURCE_DIR}/tests/wiki/test_pickup.py)
|
||||
add_test(NAME port.display
|
||||
COMMAND ${MT_WIKI_PYTHON3} ${PROJECT_SOURCE_DIR}/tests/wiki/test_display.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)
|
||||
|
||||
@@ -252,6 +252,17 @@ void CEffectManager::DeactiveEffectInstance(DWORD dwInstanceIndex)
|
||||
pEffectInstance->SetDeactive();
|
||||
}
|
||||
|
||||
// PORT: AVA2 ENABLE_GRAPHIC_ON_OFF (display options effect level).
|
||||
void CEffectManager::ActiveEffectInstance(DWORD dwInstanceIndex)
|
||||
{
|
||||
TEffectInstanceMap::iterator itor = m_kEftInstMap.find(dwInstanceIndex);
|
||||
|
||||
if (itor == m_kEftInstMap.end())
|
||||
return;
|
||||
|
||||
itor->second->SetActive();
|
||||
}
|
||||
|
||||
void CEffectManager::CreateUnsafeEffectInstance(DWORD dwEffectDataID, CEffectInstance ** ppEffectInstance)
|
||||
{
|
||||
CEffectData * pEffect;
|
||||
|
||||
@@ -47,6 +47,7 @@ class CEffectManager : public CScreen, public CSingleton<CEffectManager>
|
||||
BOOL SelectEffectInstance(DWORD dwInstanceIndex);
|
||||
bool DestroyEffectInstance(DWORD dwInstanceIndex);
|
||||
void DeactiveEffectInstance(DWORD dwInstanceIndex);
|
||||
void ActiveEffectInstance(DWORD dwInstanceIndex); // PORT: AVA2 ENABLE_GRAPHIC_ON_OFF
|
||||
|
||||
void SetEffectTextures(DWORD dwID, std::vector<std::string> textures);
|
||||
void SetEffectInstancePosition(const D3DXVECTOR3 & c_rv3Position);
|
||||
|
||||
@@ -496,6 +496,9 @@ class CActorInstance : public IActorInstance, public IFlyTargetableObject
|
||||
float GetHeight();
|
||||
void ShowAllAttachingEffect();
|
||||
void HideAllAttachingEffect();
|
||||
// PORT: AVA2 ENABLE_GRAPHIC_ON_OFF. Deactivates (not hides) attached effects, so it does not fight
|
||||
// the assassin invisibility Show/Hide; effects attached while off start deactivated.
|
||||
void SetAttachingEffectShown(bool bShown);
|
||||
void ClearAttachingEffect();
|
||||
|
||||
// Fishing
|
||||
@@ -793,6 +796,7 @@ class CActorInstance : public IActorInstance, public IFlyTargetableObject
|
||||
// Attached Effect
|
||||
std::list<TAttachingEffect> m_AttachingEffectList;
|
||||
bool m_bEffectInitialized;
|
||||
bool m_bAttachingEffectShown = true;
|
||||
|
||||
// material color
|
||||
DWORD m_dwMtrlColor;
|
||||
|
||||
@@ -294,6 +294,8 @@ DWORD CActorInstance::AttachEffectByID(DWORD dwParentPartIndex, const char * c_p
|
||||
ae.iBoneIndex = -1;
|
||||
}
|
||||
|
||||
if (!m_bAttachingEffectShown)
|
||||
rkEftMgr.DeactiveEffectInstance(ae.dwEffectIndex);
|
||||
m_AttachingEffectList.push_back(ae);
|
||||
|
||||
return ae.dwEffectIndex;
|
||||
@@ -472,6 +474,8 @@ void CActorInstance::RefreshActorInstance()
|
||||
|
||||
ae.iBoneIndex = iBoneIndex;
|
||||
|
||||
if (!m_bAttachingEffectShown)
|
||||
CEffectManager::Instance().DeactiveEffectInstance(ae.dwEffectIndex);
|
||||
m_AttachingEffectList.push_back(ae);
|
||||
}
|
||||
break;
|
||||
@@ -593,6 +597,22 @@ void CActorInstance::HideAllAttachingEffect()
|
||||
}
|
||||
}
|
||||
|
||||
void CActorInstance::SetAttachingEffectShown(bool bShown)
|
||||
{
|
||||
if (m_bAttachingEffectShown == bShown)
|
||||
return;
|
||||
|
||||
m_bAttachingEffectShown = bShown;
|
||||
CEffectManager& rkEftMgr = CEffectManager::Instance();
|
||||
for (const TAttachingEffect& ae : m_AttachingEffectList)
|
||||
{
|
||||
if (bShown)
|
||||
rkEftMgr.ActiveEffectInstance(ae.dwEffectIndex);
|
||||
else
|
||||
rkEftMgr.DeactiveEffectInstance(ae.dwEffectIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void CActorInstance::__ClearAttachingEffect()
|
||||
{
|
||||
m_bEffectInitialized = false;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "PythonNonPlayer.h"
|
||||
#include "PythonPlayer.h"
|
||||
#include "PythonCharacterManager.h"
|
||||
#include "PythonSystem.h"
|
||||
#include "AbstractPlayer.h"
|
||||
#include "AbstractApplication.h"
|
||||
#include "Packet.h"
|
||||
@@ -1868,6 +1869,7 @@ void CInstanceBase::Update()
|
||||
{
|
||||
++ms_dwUpdateCounter;
|
||||
|
||||
__UpdateEffectShown();
|
||||
StateProcess();
|
||||
m_GraphicThingInstance.PhysicsProcess();
|
||||
m_GraphicThingInstance.RotationProcess();
|
||||
@@ -2454,6 +2456,25 @@ bool CInstanceBase::__IsInViewFrustum()
|
||||
return m_GraphicThingInstance.isShow();
|
||||
}
|
||||
|
||||
// PORT: AVA2 ENABLE_GRAPHIC_ON_OFF effect level, applied to attached effects (skills, buffs, refine glow).
|
||||
void CInstanceBase::__UpdateEffectShown()
|
||||
{
|
||||
const int iLevel = CPythonSystem::Instance().GetEffectLevel();
|
||||
bool isShown = true;
|
||||
if (iLevel == 4)
|
||||
isShown = false;
|
||||
else if (iLevel != 0 && !__IsMainInstance())
|
||||
{
|
||||
if (iLevel == 1)
|
||||
isShown = IsEnemy() || IsStone();
|
||||
else if (iLevel == 2)
|
||||
isShown = IsPC();
|
||||
else
|
||||
isShown = false;
|
||||
}
|
||||
m_GraphicThingInstance.SetAttachingEffectShown(isShown);
|
||||
}
|
||||
|
||||
bool CInstanceBase::__CanRender()
|
||||
{
|
||||
if (!__IsInViewFrustum())
|
||||
|
||||
@@ -407,6 +407,7 @@ class CInstanceBase
|
||||
protected:
|
||||
bool __IsExistMainInstance();
|
||||
bool __IsMainInstance();
|
||||
void __UpdateEffectShown();
|
||||
bool __MainCanSeeHiddenThing();
|
||||
float __GetBowRange();
|
||||
|
||||
|
||||
@@ -110,6 +110,9 @@ class CPythonSystem : public CSingleton<CPythonSystem>
|
||||
bool IsAlwaysShowName();
|
||||
bool IsShowDamage();
|
||||
bool IsShowSalesText();
|
||||
// PORT: AVA2 effect_level (0 all, 1 hide players/NPCs, 2 hide monsters/NPCs, 3 hide others, 4 hide all).
|
||||
int GetEffectLevel() { return m_iPortEffectLevel; }
|
||||
void SetEffectLevel(int iLevel) { m_iPortEffectLevel = iLevel < 0 || iLevel > 4 ? 0 : iLevel; }
|
||||
bool IsUseDefaultIME();
|
||||
bool IsNoSoundCard();
|
||||
bool IsAutoTiling();
|
||||
@@ -159,4 +162,5 @@ class CPythonSystem : public CSingleton<CPythonSystem>
|
||||
bool m_isInterfaceConfig;
|
||||
PyObject * m_poInterfaceHandler;
|
||||
TWindowStatus m_WindowStatus[WINDOW_MAX_NUM];
|
||||
int m_iPortEffectLevel = 0;
|
||||
};
|
||||
@@ -211,6 +211,22 @@ PyObject * systemSetShowSalesTextFlag(PyObject * poSelf, PyObject * poArgs)
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject * systemSetEffectLevel(PyObject * poSelf, PyObject * poArgs)
|
||||
{
|
||||
int iLevel;
|
||||
if (!PyTuple_GetInteger(poArgs, 0, &iLevel))
|
||||
return Py_BuildException();
|
||||
|
||||
CPythonSystem::Instance().SetEffectLevel(iLevel);
|
||||
|
||||
return Py_BuildNone();
|
||||
}
|
||||
|
||||
PyObject * systemGetEffectLevel(PyObject * poSelf, PyObject * poArgs)
|
||||
{
|
||||
return Py_BuildValue("i", CPythonSystem::Instance().GetEffectLevel());
|
||||
}
|
||||
|
||||
PyObject * systemIsAlwaysShowName(PyObject * poSelf, PyObject * poArgs)
|
||||
{
|
||||
return Py_BuildValue("i", CPythonSystem::Instance().IsAlwaysShowName());
|
||||
@@ -422,6 +438,8 @@ void initsystem()
|
||||
|
||||
{ "SetShowSalesTextFlag", systemSetShowSalesTextFlag, METH_VARARGS },
|
||||
{ "IsShowSalesText", systemIsShowSalesText, METH_VARARGS },
|
||||
{ "SetEffectLevel", systemSetEffectLevel, METH_VARARGS }, // PORT: AVA2
|
||||
{ "GetEffectLevel", systemGetEffectLevel, METH_VARARGS },
|
||||
|
||||
{ "GetShadowLevel", systemGetShadowLevel, METH_VARARGS },
|
||||
{ "SetShadowLevel", systemSetShadowLevel, METH_VARARGS },
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
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.shown=False;self.text='';self.pos=(0,0);self.size=(0,0)
|
||||
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
|
||||
|
||||
|
||||
class DisplayTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.cwd=os.getcwd();self.tmp=tempfile.TemporaryDirectory();os.chdir(self.tmp.name)
|
||||
self.state=dict(effect=None,name=True,damage=True,parts={});self.opened=[]
|
||||
mods={n:types.ModuleType(n) for n in ('background','game','grp','systemSetting','ui','mt_biolog','mt_wiki')}
|
||||
bg=mods['background'];bg.PART_TREE=3;bg.PART_CLOUD=2;bg.SetVisiblePart=lambda part,on:self.state['parts'].__setitem__(part,on)
|
||||
ss=mods['systemSetting'];ss.SetEffectLevel=lambda v:self.state.__setitem__('effect',v)
|
||||
ss.IsAlwaysShowName=lambda:self.state['name'];ss.SetAlwaysShowNameFlag=lambda v:self.state.__setitem__('name',bool(v))
|
||||
ss.IsShowDamage=lambda:self.state['damage'];ss.SetShowDamageFlag=lambda v:self.state.__setitem__('damage',bool(v))
|
||||
mods['game'].GameWindow=type('GameWindow',(),dict(Open=lambda s:self.opened.append(1),Close=lambda s:None))
|
||||
grp=mods['grp'];grp.SetColor=grp.RenderBar=grp.RenderBox=lambda *a:None
|
||||
mods['ui'].Window=Widget
|
||||
mods['mt_biolog'].Action=Action;mods['mt_biolog'].Text=lambda p,x,y,t='',c=0:Widget();mods['mt_biolog'].L=lambda t:t if isinstance(t,str) else t.decode('utf-8')
|
||||
mods['mt_wiki'].B=lambda t,limit=0:t
|
||||
self.previous={n:sys.modules.get(n) for n in mods}
|
||||
sys.modules.update(mods)
|
||||
|
||||
def load(self):
|
||||
self.ns={'__name__':'mt_display'};exec(script('DisplayScript.inc'),self.ns);return self.ns
|
||||
|
||||
def tearDown(self):
|
||||
os.chdir(self.cwd);self.tmp.cleanup()
|
||||
for name,value in self.previous.items():
|
||||
if value is None:sys.modules.pop(name,None)
|
||||
else:sys.modules[name]=value
|
||||
|
||||
def test_defaults_and_config(self):
|
||||
ns=self.load();self.assertEqual(self.state['effect'],0)
|
||||
o=ns['Options']();self.assertEqual(o.Dump(),'0 1 1')
|
||||
for bad in ('5 1 1','1 2 1','a 1 1','1 1','10 1 1','-1 1 1'):
|
||||
o=ns['Options']();o.Load(bad);self.assertEqual(o.Dump(),'0 1 1')
|
||||
Path('mt_display.cfg').write_text('3 0 1')
|
||||
self.load();self.assertEqual(self.state['effect'],3) # applied at boot, before any map
|
||||
|
||||
def test_game_window_applies_and_saves(self):
|
||||
Path('mt_display.cfg').write_text('2 0 1')
|
||||
ns=self.load();game=sys.modules['game'];gw=game.GameWindow();gw.interface=types.SimpleNamespace(wndMiniMap=Widget())
|
||||
game.GameWindow.Open(gw);self.assertEqual(self.opened,[1])
|
||||
self.assertEqual(self.state['parts'],{3:0,2:1});self.assertEqual(ns['g_entry'].pos,(4,252))
|
||||
w=ns['g_window'];w.Open()
|
||||
self.assertIn('【',w.levelButtons[2].text);self.assertNotIn('【',w.levelButtons[0].text)
|
||||
self.assertNotIn('【',w.treeButton.text);self.assertIn('【',w.cloudButton.text)
|
||||
w.levelButtons[4].event();self.assertEqual(self.state['effect'],4)
|
||||
w.ToggleTree();w.ToggleCloud();self.assertEqual(self.state['parts'],{3:1,2:0})
|
||||
self.assertEqual(Path('mt_display.cfg').read_text(),'4 1 0')
|
||||
w.ToggleName();w.ToggleDamage();self.assertEqual((self.state['name'],self.state['damage']),(False,False))
|
||||
self.assertNotIn('【',w.nameButton.text)
|
||||
game.GameWindow.Close(gw);self.assertIsNone(ns['g_window']);self.assertIsNone(ns['g_entry'])
|
||||
|
||||
def test_levels_match_native_rule(self):
|
||||
ns=self.load();text=(ROOT/'src/port/UserInterface/InstanceBase.cpp').read_text(encoding='latin1')
|
||||
body=text.split('void CInstanceBase::__UpdateEffectShown()',1)[1].split('\n}\n',1)[0]
|
||||
self.assertEqual(len(ns['LEVELS']),5);self.assertIn('iLevel == 4',body)
|
||||
self.assertIn('IsEnemy() || IsStone()',body);self.assertIn('IsPC()',body)
|
||||
system=(ROOT/'src/port/UserInterface/PythonSystem.h').read_text(encoding='latin1')
|
||||
self.assertIn('iLevel > 4',system)
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
Reference in New Issue
Block a user