android-native: auto hunt, in-client account registration, mobile UI polish

- Auto hunt (client-only): PythonPlayerAutoHunt.cpp + AutoHuntScript.inc as the
  embedded mt_autohunt module, AUTO touch button, F9/F8.
- Account registration: tools/40250/register_server.py (rc.d mt_register, :11080)
  inserts into account.account; net.RegisterAccount/GetRegisterAccountResult
  (PythonAccountRegister.cpp) and the mt_register login-window dialog
  (RegisterScript.inc).
- --py-exec also runs in --login-screen mode for scripted login-screen tests.
- Touch controller, window manager and render command updates for the
  Android native client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-29 16:15:04 +09:00
co-authored by Claude Opus 5.5
parent 53d06c3c26
commit 3e3708ef06
31 changed files with 2975 additions and 140 deletions
@@ -5,6 +5,10 @@ import android.content.pm.ActivityInfo;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Display;
import android.view.DisplayCutout;
import android.view.HapticFeedbackConstants;
import android.view.RoundedCorner;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
@@ -35,6 +39,11 @@ public class MainActivity extends SDLActivity {
applyImmersiveMode();
}
// Called from native code (touch_controller long press).
public void performHaptic() {
runOnUiThread(() -> getWindow().getDecorView().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS));
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
@@ -75,9 +84,45 @@ public class MainActivity extends SDLActivity {
Log.i("Metin2Args", "native args: " + args);
if (args == null || args.trim().isEmpty())
args = DEFAULT_ARGS;
if (!args.contains("--safe-inset-px"))
args = args.trim() + " --safe-inset-px " + safeInsetPx();
// Test phase: FPS readout in the top-left corner. Drop this line for release builds.
if (!args.contains("--show-fps"))
args = args.trim() + " --show-fps";
return args.trim().split("\\s+");
}
/**
* Pixels the game UI keeps clear at the left and right edges in landscape: the largest rounded
* screen corner (a window hugging a corner is clipped until it is a full radius in) and any
* display cutout. The 3D view still fills the screen.
*/
private int safeInsetPx() {
int inset = 0;
final Display display = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ? getDisplay()
: getWindowManager().getDefaultDisplay();
if (display == null)
return 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
final int[] positions = { RoundedCorner.POSITION_TOP_LEFT, RoundedCorner.POSITION_TOP_RIGHT,
RoundedCorner.POSITION_BOTTOM_LEFT, RoundedCorner.POSITION_BOTTOM_RIGHT };
for (int position : positions) {
final RoundedCorner corner = display.getRoundedCorner(position);
if (corner != null)
inset = Math.max(inset, corner.getRadius());
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// The activity may not be landscape yet: any side's cutout can end up left or right.
final DisplayCutout cutout = display.getCutout();
if (cutout != null)
inset = Math.max(inset, Math.max(Math.max(cutout.getSafeInsetLeft(), cutout.getSafeInsetRight()),
Math.max(cutout.getSafeInsetTop(), cutout.getSafeInsetBottom())));
}
Log.i("Metin2Args", "safe inset px: " + inset);
return inset;
}
@Override
public void setOrientationBis(int w, int h, boolean resizable, String hint) {
// Enforce landscape-only on mobile devices (sensorLandscape allows flipping 180 deg but never portrait).
@@ -816,6 +816,7 @@ private:
draw.viewport[3] = float(m_viewport.Height);
draw.viewport_z[0] = m_viewport.MinZ;
draw.viewport_z[1] = m_viewport.MaxZ;
draw.ui_offset_x = UIRenderOffsetX();
draw.pretransformed = layout.rhw;
draw.lines = type == D3DPT_LINELIST || type == D3DPT_LINESTRIP;
@@ -19,6 +19,9 @@ struct Render3DDraw {
float proj[16];
float viewport[4] = {}; // x, y, width, height
float viewport_z[2] = {0, 1}; // MinZ, MaxZ
// PORT: UIRenderOffsetX() when recorded (a draw from a shifted window layer); the host moves the
// viewport (and a pretransformed draw's screen positions) right by it.
float ui_offset_x = 0;
// IDirect3DDevice8::Clear on the back buffer, kept in draw order. A clear entry has no geometry;
// clear_flags holds D3DCLEAR_TARGET / D3DCLEAR_ZBUFFER / D3DCLEAR_STENCIL.
@@ -34,8 +34,12 @@ int canvas_width = 0;
int canvas_height = 0;
float clip_x1 = 0, clip_y1 = 0, clip_x2 = 0, clip_y2 = 0;
float saved_x1 = 0, saved_y1 = 0, saved_x2 = 0, saved_y2 = 0;
float offset_x = 0;
}
void UIRenderSetOffsetX(float x) { offset_x = x; }
float UIRenderOffsetX() { return offset_x; }
void UIRenderSetSize(int width, int height) { canvas_width = width; canvas_height = height; }
void UIRenderGetSize(unsigned* width, unsigned* height) {
if (width) *width = canvas_width;
@@ -50,6 +54,12 @@ void UIRenderBeginFrame() {
}
std::uint64_t UIRenderFrameId() { return frame_id; }
void UIRenderAdd(UIRenderCommand command) {
if (offset_x != 0) {
command.x1 += offset_x;
command.x2 += offset_x;
if (command.quad)
for (float& x : command.qx) x += offset_x;
}
command.clip_x1 = clip_x1; command.clip_y1 = clip_y1;
command.clip_x2 = clip_x2; command.clip_y2 = clip_y2;
command.behind_3d = Render3DDraws().empty();
@@ -84,8 +94,8 @@ void UIRenderAdd(UIRenderCommand command) {
const std::vector<UIRenderCommand>& UIRenderCommands() { return commands; }
void UIRenderSetClip(float x, float y, float width, float height) {
saved_x1 = clip_x1; saved_y1 = clip_y1; saved_x2 = clip_x2; saved_y2 = clip_y2;
clip_x1 = x; clip_y1 = y;
clip_x2 = x + width; clip_y2 = y + height;
clip_x1 = x + offset_x; clip_y1 = y;
clip_x2 = x + offset_x + width; clip_y2 = y + height;
}
void UIRenderRestoreClip() {
clip_x1 = saved_x1; clip_y1 = saved_y1;
@@ -71,6 +71,11 @@ std::uint64_t UIRenderFrameId();
void UIRenderAdd(UIRenderCommand command);
const std::vector<UIRenderCommand>& UIRenderCommands();
void UIRenderSetClip(float x, float y, float width, float height);
// PORT (mobile safe area): shifts the commands (and clips) recorded after it right by x canvas
// pixels, so the 40250 window layers can sit inside the rounded screen corners while the GAME layer
// (3D world, text tails) keeps the full canvas. 0 is the verbatim 40250 path.
void UIRenderSetOffsetX(float x);
float UIRenderOffsetX();
void UIRenderRestoreClip();
// HiDPI text: CGraphicFontTexture rasterises its glyph pages at this multiple of the logical font
@@ -0,0 +1,385 @@
// The auto hunt window (PORT; 40250 has no counterpart), run by PythonBoot::RunMainScript as module
// mt_autohunt. It hooks game.GameWindow.Open/Close so the window lives exactly as long as the game phase,
// whichever way the host reached it. The logic is CPythonPlayer's (UserInterface/PythonPlayerAutoHunt.cpp);
// this is only the player.AutoHunt* front end. Strings are UTF-8 here and go through
// player.AutoHuntLocalize into the locale's code page, as the packed locale strings are.
static const char c_szAutoHuntScript[] = R"PY(# -*- coding: utf-8 -*-
import app
import chat
import game
import player
import ui
import wndMgr
L = player.AutoHuntLocalize
STATE_OFF, STATE_SEARCH, STATE_ENGAGE, STATE_LOOT, STATE_RETURN, STATE_PAUSED = range(6)
STOP_NONE, STOP_USER, STOP_DEAD, STOP_MAP, STOP_INVENTORY_FULL = range(5)
SKILL_SLOT_NUM = 8
STATE_TEXT = {
STATE_SEARCH: "寻找目标",
STATE_ENGAGE: "战斗中",
STATE_LOOT: "拾取中",
STATE_RETURN: "返回挂机点",
STATE_PAUSED: "手动操作,3秒后继续",
}
STOP_TEXT = {
STOP_USER: "自动狩猎已停止",
STOP_DEAD: "角色死亡,自动狩猎已停止",
STOP_MAP: "切换地图,自动狩猎已停止",
STOP_INVENTORY_FULL: "背包已满,自动狩猎已停止",
}
RANGE_VALUES = (1000, 2000, 3500)
RANGE_TEXTS = ("近", "中", "远")
HP_VALUES = (0, 30, 50, 70)
SP_VALUES = (0, 20, 40, 60)
PICKUP_TEXTS = ("不拾取", "全部", "金币装备")
GATHER_VALUES = (0, 3, 5, 8)
GATHER_TEXTS = ("关", "3只", "5只", "8只")
SMALL_BUTTON = "d:/ymir work/ui/public/small_button_%02d.sub"
MIDDLE_BUTTON = "d:/ymir work/ui/public/middle_button_%02d.sub"
XSMALL_BUTTON = "d:/ymir work/ui/public/xsmall_button_%02d.sub"
COLOR_ON = 0xff7fff7f
COLOR_OFF = 0xffffcf7f
COLOR_SELECTED = 0xffffd24a
COLOR_UNSELECTED = 0xffc8c8c8
def _MarkButton(button, isSelected):
"Down and gold text while selected: the down visual alone is hard to tell apart on a phone."
if isSelected:
button.Down()
else:
button.SetUp()
if button.ButtonText:
button.ButtonText.SetPackedFontColor(isSelected and COLOR_SELECTED or COLOR_UNSELECTED)
def _Nearest(values, value):
best = 0
for i in xrange(len(values)):
if abs(values[i] - value) < abs(values[best] - value):
best = i
return best
class ChoiceRow:
"One setting: a label and a row of buttons of which exactly one stays down."
def __init__(self, parent, y, label, texts, image, step, event):
self.label = ui.TextLine()
self.label.SetParent(parent)
self.label.SetPosition(18, y + 4)
self.label.SetText(L(label))
self.label.Show()
self.buttons = []
self.event = event
self.index = -1
x = 70
for i in xrange(len(texts)):
button = ui.RadioButton()
button.SetParent(parent)
button.SetUpVisual(image % 1)
button.SetOverVisual(image % 2)
button.SetDownVisual(image % 3)
button.SetPosition(x, y)
button.SetText(L(texts[i]))
button.SetEvent(lambda i=i: self.Select(i, True))
button.Show()
self.buttons.append(button)
x += step
def Select(self, index, notify):
self.index = index
for i in xrange(len(self.buttons)):
_MarkButton(self.buttons[i], i == index)
if notify and self.event:
self.event()
def Destroy(self):
self.event = None
self.buttons = []
self.label = None
class FlagButton(ui.ToggleButton):
"A button that stays down while its flag is on."
def __init__(self, parent, x, y, image, text, event):
ui.ToggleButton.__init__(self)
self.SetParent(parent)
self.SetUpVisual(image % 1)
self.SetOverVisual(image % 2)
self.SetDownVisual(image % 3)
self.SetPosition(x, y)
self.SetText(L(text))
self.flag = False
self.flagEvent = event
self.SetToggleDownEvent(lambda: self.__OnToggle(True))
self.SetToggleUpEvent(lambda: self.__OnToggle(False))
self.Show()
def __OnToggle(self, flag):
self.SetFlag(flag)
if self.flagEvent:
self.flagEvent()
def SetFlag(self, flag):
self.flag = flag
_MarkButton(self, flag)
def Destroy(self):
self.flagEvent = None
self.eventUp = None
self.eventDown = None
class SettingDialog(ui.BoardWithTitleBar):
WIDTH = 330
HEIGHT = 300
def __init__(self):
ui.BoardWithTitleBar.__init__(self)
self.AddFlag("movable")
self.AddFlag("float")
self.SetSize(self.WIDTH, self.HEIGHT)
self.SetTitleName(L("自动狩猎"))
self.SetCloseEvent(self.Close)
y = 36
self.rangeRow = ChoiceRow(self, y, "范围", RANGE_TEXTS, SMALL_BUTTON, 48, self.__Save)
y += 28
self.hpRow = ChoiceRow(self, y, "红药", ["关"] + ["%d%%" % v for v in HP_VALUES[1:]], SMALL_BUTTON, 48, self.__Save)
y += 28
self.spRow = ChoiceRow(self, y, "蓝药", ["关"] + ["%d%%" % v for v in SP_VALUES[1:]], SMALL_BUTTON, 48, self.__Save)
y += 28
self.pickupRow = ChoiceRow(self, y, "拾取", PICKUP_TEXTS, MIDDLE_BUTTON, 66, self.__Save)
y += 28
self.gatherRow = ChoiceRow(self, y, "聚怪", GATHER_TEXTS, SMALL_BUTTON, 48, self.__Save)
y += 28
self.skillLabel = ui.TextLine()
self.skillLabel.SetParent(self)
self.skillLabel.SetPosition(18, y + 4)
self.skillLabel.SetText(L("技能栏"))
self.skillLabel.Show()
self.skillButtons = []
for i in xrange(SKILL_SLOT_NUM):
self.skillButtons.append(FlagButton(self, 70 + i * 31, y, XSMALL_BUTTON, str(i + 1), self.__Save))
y += 28
self.targetLabel = ui.TextLine()
self.targetLabel.SetParent(self)
self.targetLabel.SetPosition(18, y + 4)
self.targetLabel.SetText(L("攻击"))
self.targetLabel.Show()
self.stoneButton = FlagButton(self, 70, y, MIDDLE_BUTTON, "石头", self.__Save)
self.bossButton = FlagButton(self, 136, y, MIDDLE_BUTTON, "首领", self.__Save)
y += 30
self.hintLine = ui.TextLine()
self.hintLine.SetParent(self)
self.hintLine.SetPosition(0, y + 2)
self.hintLine.SetWindowHorizontalAlignCenter()
self.hintLine.SetHorizontalAlignCenter()
self.hintLine.SetPackedFontColor(0xffa0a0a0)
self.hintLine.SetText(L("手动操作时暂停,3秒后继续"))
self.hintLine.Show()
y += 26
self.startButton = ui.Button()
self.startButton.SetParent(self)
self.startButton.SetUpVisual("d:/ymir work/ui/public/large_button_01.sub")
self.startButton.SetOverVisual("d:/ymir work/ui/public/large_button_02.sub")
self.startButton.SetDownVisual("d:/ymir work/ui/public/large_button_03.sub")
self.startButton.SetPosition(0, y)
self.startButton.SetWindowHorizontalAlignCenter()
self.startButton.SetEvent(self.__OnClickStart)
self.startButton.Show()
self.SetSize(self.WIDTH, y + 36)
self.SetCenterPosition()
self.isStarted = None
def Destroy(self):
for row in (self.rangeRow, self.hpRow, self.spRow, self.pickupRow, self.gatherRow):
row.Destroy()
for button in self.skillButtons + [self.stoneButton, self.bossButton]:
button.Destroy()
self.skillButtons = []
self.startButton = None
self.Hide()
def Open(self):
(range_, hp, sp, pickup, stone, boss, mask, gather) = player.AutoHuntGetConfig()
self.rangeRow.Select(_Nearest(RANGE_VALUES, range_), False)
self.hpRow.Select(_Nearest(HP_VALUES, hp), False)
self.spRow.Select(_Nearest(SP_VALUES, sp), False)
self.pickupRow.Select(pickup, False)
self.gatherRow.Select(_Nearest(GATHER_VALUES, gather), False)
for i in xrange(SKILL_SLOT_NUM):
self.skillButtons[i].SetFlag(bool(mask & (1 << i)))
self.stoneButton.SetFlag(bool(stone))
self.bossButton.SetFlag(bool(boss))
self.isStarted = None
self.Show()
self.SetTop()
def Close(self):
self.Hide()
def OnPressEscapeKey(self):
self.Close()
return True
def OnUpdate(self):
isStarted = bool(player.AutoHuntIsEnabled())
if isStarted != self.isStarted:
self.isStarted = isStarted
self.startButton.SetText(L(isStarted and "停止狩猎" or "开始狩猎"))
def __Save(self):
mask = 0
for i in xrange(SKILL_SLOT_NUM):
if self.skillButtons[i].flag:
mask |= 1 << i
player.AutoHuntSetConfig(RANGE_VALUES[self.rangeRow.index], HP_VALUES[self.hpRow.index],
SP_VALUES[self.spRow.index], self.pickupRow.index, int(self.stoneButton.flag), int(self.bossButton.flag), mask,
GATHER_VALUES[self.gatherRow.index])
def __OnClickStart(self):
Toggle()
if player.AutoHuntIsEnabled():
self.Close()
class StatusBoard(ui.ThinBoard):
def __init__(self):
ui.ThinBoard.__init__(self)
self.AddFlag("not_pick")
self.SetSize(260, 28)
self.SetWindowHorizontalAlignCenter()
self.SetPosition(0, 64)
self.textLine = ui.TextLine()
self.textLine.SetParent(self)
self.textLine.SetPosition(0, 7)
self.textLine.SetWindowHorizontalAlignCenter()
self.textLine.SetHorizontalAlignCenter()
self.textLine.Show()
def SetStatus(self, text, color):
self.textLine.SetText(text)
self.textLine.SetPackedFontColor(color)
self.Show()
class Ticker(ui.Window):
"Always shown and empty: polls the hunt state for the status board, which is hidden while off."
def __init__(self, status):
ui.Window.__init__(self)
self.AddFlag("not_pick")
self.status = status
self.last = None
self.stopShowTime = 0.0
def OnUpdate(self):
state = player.AutoHuntGetState()
if state != self.last:
(stateIndex, reason, noHP, noSP, gathering, gathered) = state
wasOn = self.last and self.last[0] != STATE_OFF
self.last = state
if stateIndex != STATE_OFF:
text = L("自动狩猎 · ") + L(STATE_TEXT.get(stateIndex, ""))
if gathering:
text = L("自动狩猎 · 聚怪中 %d/%d" % (gathered, player.AutoHuntGetConfig()[7]))
if noHP:
text += L(" · 无红药")
if noSP:
text += L(" · 无蓝药")
self.status.SetStatus(text, COLOR_ON)
elif wasOn and reason in STOP_TEXT:
self.status.SetStatus(L(STOP_TEXT[reason]), COLOR_OFF)
self.stopShowTime = app.GetTime() + 5.0
if reason != STOP_USER:
chat.AppendChat(chat.CHAT_TYPE_INFO, L(STOP_TEXT[reason]))
else:
self.status.Hide()
elif state[0] == STATE_OFF and self.stopShowTime and app.GetTime() > self.stopShowTime:
self.stopShowTime = 0.0
self.status.Hide()
g_status = None
g_ticker = None
g_dialog = None
def Toggle():
if player.AutoHuntIsEnabled():
player.AutoHuntSetEnabled(0)
return
player.AutoHuntSetEnabled(1)
if player.AutoHuntIsEnabled():
chat.AppendChat(chat.CHAT_TYPE_INFO, L("自动狩猎已开启(再次点击AUTO或F9停止)"))
def OpenSettings():
if g_dialog:
if g_dialog.IsShow():
g_dialog.Close()
else:
g_dialog.Open()
def IsSettingsOpen():
return g_dialog and g_dialog.IsShow()
def Attach(gameWindow):
global g_status, g_ticker, g_dialog
Detach()
g_status = StatusBoard()
g_ticker = Ticker(g_status)
g_ticker.Show()
g_dialog = SettingDialog()
if gameWindow.onPressKeyDict is not None:
gameWindow.onPressKeyDict[app.DIK_F9] = Toggle
gameWindow.onPressKeyDict[app.DIK_F8] = OpenSettings
def Detach():
global g_status, g_ticker, g_dialog
player.AutoHuntSetEnabled(0)
if g_dialog:
g_dialog.Destroy()
if g_ticker:
g_ticker.Hide()
g_ticker.status = None
if g_status:
g_status.Hide()
g_status = g_ticker = g_dialog = None
def __Install():
openGame = game.GameWindow.Open
closeGame = game.GameWindow.Close
def Open(self):
openGame(self)
Attach(self)
def Close(self):
Detach()
closeGame(self)
game.GameWindow.Open = Open
game.GameWindow.Close = Close
__Install()
)PY";
+187 -38
View File
@@ -5,6 +5,7 @@
#include "ScriptLib/Resource.h"
#include "UserInterface/StdAfx.h" // initpack and the rest of the module initializer list
#include "EterPythonLib/StdAfx.h"
#include "EterPythonLib/PythonWindow.h"
#include "EterBase/Timer.h"
#include "EterLib/Util.h"
#include "EterLib/GrpBase.h"
@@ -151,6 +152,7 @@ struct GameSingletons
};
std::unique_ptr<GameSingletons> g_game_singletons;
bool g_host_hardware_cursor_enabled = false;
bool g_touch_input = false;
bool fail(std::string* error, const std::string& text)
{
@@ -268,11 +270,20 @@ bool IsRunning()
return g_launcher != nullptr;
}
static int g_ui_safe_inset = 0;
void SetUISafeInset(int inset)
{
g_ui_safe_inset = std::max(0, inset);
}
void SetUISize(int width, int height)
{
if (!g_window_manager || width <= 0 || height <= 0) return;
g_window_manager->SetScreenSize(width, height);
const int inset = std::min(g_ui_safe_inset, width / 8);
g_window_manager->SetScreenSize(width - 2 * inset, height);
g_window_manager->SetResolution(width, height);
g_window_manager->SetScreenOffsetX(inset);
UIRenderSetSize(width, height);
CGraphicBase::SetBackBufferSize(width, height);
if (g_game_singletons)
@@ -290,8 +301,10 @@ void update_3d_pick_ray()
return;
long lx = 0, ly = 0;
g_window_manager->GetMousePosition(lx, ly);
const long sw = g_window_manager->GetScreenWidth();
const long sh = g_window_manager->GetScreenHeight();
// The pick ray goes through the full-screen 3D view: screen coordinates and resolution.
lx += g_window_manager->GetScreenOffsetX();
long sw = 0, sh = 0;
g_window_manager->GetResolution(sw, sh);
if (sw <= 0 || sh <= 0)
return;
CScreen s;
@@ -330,6 +343,11 @@ void UIMouseButton(int button, bool pressed, int x, int y)
CPythonApplication::Instance().OnMouseMove(x, y);
else
g_window_manager->RunMouseMove(x, y);
{
long lx = x, ly = y;
g_window_manager->GetMousePosition(lx, ly);
MtHostSetCursor(static_cast<int>(lx), static_cast<int>(ly));
}
update_3d_pick_ray();
switch (button)
{
@@ -398,7 +416,8 @@ void SetMoveDirection(float angleDeg, bool moving)
{
if (!g_game_singletons) return;
if (moving)
CPythonPlayer::Instance().NEW_MoveToDirection(angleDeg);
// The touch stick only steers the player; the camera turns by the right-hand drag.
CPythonPlayer::Instance().NEW_MoveToDirection(angleDeg, false);
else
CPythonPlayer::Instance().NEW_Stop();
}
@@ -415,6 +434,30 @@ void UseLocalQuickSlot(int localSlotIndex)
CPythonPlayer::Instance().RequestUseLocalQuickSlot(static_cast<DWORD>(localSlotIndex));
}
void AutoHuntToggle()
{
std::string error;
if (g_game_singletons)
RunLine("__import__('mt_autohunt').Toggle()", &error);
}
void AutoHuntOpenSettings()
{
std::string error;
if (g_game_singletons)
RunLine("__import__('mt_autohunt').OpenSettings()", &error);
}
bool AutoHuntIsEnabled()
{
return g_game_singletons && CPythonPlayer::Instance().AutoHunt_IsEnabled();
}
bool AutoHuntAvailable()
{
return g_game_singletons && CPythonPlayer::Instance().NEW_GetMainActorPtr() != nullptr;
}
bool TryAssignAttachedObjectToLocalQuickSlot(int localSlotIndex)
{
if (!g_game_singletons || localSlotIndex < 0 || localSlotIndex >= 8) return false;
@@ -429,55 +472,130 @@ bool TryAssignAttachedObjectToLocalQuickSlot(int localSlotIndex)
return RunLine(command.c_str(), &error);
}
std::string LocalQuickSlotSkillIcon(int localSlotIndex)
namespace
{
if (!g_game_singletons || localSlotIndex < 0 || localSlotIndex >= 8) return {};
// The icons are .sub images (a rect of a shared texture). A resource only loads while referenced, and
// on touch hosts the taskbar slots that would reference it are hidden, so the overlay holds one.
bool quick_slot_image_uv(CGraphicImage* image, std::string* texture, float uv[4])
{
if (!image) return false;
static std::vector<CGraphicImage::TRef> s_heldIcons;
if (std::none_of(s_heldIcons.begin(), s_heldIcons.end(),
[image](const CGraphicImage::TRef& ref) { return ref.GetPointer() == image; }))
s_heldIcons.emplace_back(image);
CGraphicTexture* tex = image->GetTexturePointer();
if (image->IsEmpty() || !tex || tex->GetWidth() <= 0 || tex->GetHeight() <= 0) return false;
*texture = UIRenderTextureName(tex);
if (texture->empty()) return false;
// CGraphicImageInstance::OnRender's texture coordinates.
const RECT& rc = image->GetRectReference();
uv[0] = float(rc.left) / float(tex->GetWidth());
uv[1] = float(rc.top) / float(tex->GetHeight());
uv[2] = float(rc.right) / float(tex->GetWidth());
uv[3] = float(rc.bottom) / float(tex->GetHeight());
return true;
}
}
bool LocalQuickSlotIcon(int localSlotIndex, std::string* texture, float uv[4], int* count)
{
if (!g_game_singletons || localSlotIndex < 0 || localSlotIndex >= 8) return false;
DWORD type = SLOT_TYPE_NONE;
DWORD position = 0;
CPythonPlayer& player = CPythonPlayer::Instance();
player.GetLocalQuickSlotData(static_cast<DWORD>(localSlotIndex), &type, &position);
if (type != SLOT_TYPE_SKILL) return {};
*count = 0;
CPythonSkill::TSkillData* skillData = nullptr;
if (!CPythonSkill::Instance().GetSkillData(player.GetSkillIndex(position), &skillData) || !skillData)
return {};
const int grade = std::clamp(player.GetSkillGrade(position), 0, CPythonSkill::SKILL_GRADE_COUNT - 1);
CGraphicImage* image = skillData->GradeData[grade].pImage;
if (!image) image = skillData->pImage;
return image ? image->GetFileName() : std::string{};
}
void CameraBeginDrag(int x, int y)
{
CCameraManager& rkCmrMgr = CCameraManager::Instance();
CCamera* pkCmrCur = rkCmrMgr.GetCurrentCamera();
if (pkCmrCur)
pkCmrCur->BeginDrag(x, y);
}
void CameraDrag(int x, int y)
{
CCameraManager& rkCmrMgr = CCameraManager::Instance();
CCamera* pkCmrCur = rkCmrMgr.GetCurrentCamera();
if (pkCmrCur)
{
POINT pt;
pkCmrCur->Drag(x, y, &pt);
if (type == SLOT_TYPE_SKILL) {
CPythonSkill::TSkillData* skillData = nullptr;
if (!CPythonSkill::Instance().GetSkillData(player.GetSkillIndex(position), &skillData) || !skillData)
return false;
const int grade = std::clamp(player.GetSkillGrade(position), 0, CPythonSkill::SKILL_GRADE_COUNT - 1);
CGraphicImage* image = skillData->GradeData[grade].pImage;
if (!image) image = skillData->pImage;
return quick_slot_image_uv(image, texture, uv);
}
if (type == SLOT_TYPE_INVENTORY) {
// uitaskbar.RefreshQuickSlot: the item's icon and, above one, its count.
const TItemPos cell(INVENTORY, static_cast<WORD>(position));
CItemData* itemData = nullptr;
if (!CItemManager::Instance().GetItemDataPointer(player.GetItemIndex(cell), &itemData) || !itemData)
return false;
const DWORD itemCount = player.GetItemCount(cell);
*count = itemCount > 1 ? static_cast<int>(itemCount) : 0;
return quick_slot_image_uv(itemData->GetIconImage(), texture, uv);
}
return false;
}
void CameraEndDrag()
void UIMouseDoubleClick(int x, int y)
{
CCameraManager& rkCmrMgr = CCameraManager::Instance();
CCamera* pkCmrCur = rkCmrMgr.GetCurrentCamera();
if (pkCmrCur)
pkCmrCur->EndDrag();
MtHostSetCursor(x, y);
if (!g_window_manager) return;
if (g_game_singletons)
CPythonApplication::Instance().OnMouseMove(x, y);
else
g_window_manager->RunMouseMove(x, y);
update_3d_pick_ray();
g_window_manager->RunMouseLeftButtonDoubleClick(x, y);
}
bool UIIsAttaching()
{
return g_window_manager && g_window_manager->IsAttaching();
}
void UIDeattachObject()
{
std::string error;
RunLine("__import__('mouseModule').mouseController.DeattachObject()", &error);
}
void CameraRotateBy(float rollDeg, float pitchDeg)
{
CCamera* pkCmrCur = CCameraManager::Instance().GetCurrentCamera();
if (!pkCmrCur || pkCmrCur->IsLock())
return;
// Same collision limits CCamera::Drag applies to its velocities.
switch (pkCmrCur->GetCameraState())
{
case CAMERA_STATE_CANTGOLEFT: rollDeg = std::max(0.0f, rollDeg); break;
case CAMERA_STATE_CANTGORIGHT: rollDeg = std::min(0.0f, rollDeg); break;
case CAMERA_STATE_CANTGODOWN: pitchDeg = std::max(0.0f, pitchDeg); break;
default: break;
}
pkCmrCur->RotateEyeAroundTarget(pitchDeg, rollDeg);
}
void SetTouchInput(bool touch)
{
g_touch_input = touch;
UI::CWindow::SetHideVirtualKeyboard(touch);
}
bool TouchInput()
{
return g_touch_input;
}
bool TextInputFocused()
{
return g_window_manager && CIME::ms_bCaptureInput;
}
bool IsPointInsideFocusedWindow(int x, int y)
{
if (!g_window_manager) return false;
UI::CWindow* pWin = g_window_manager->GetActivateWindow();
return pWin && pWin->IsShow() && pWin->IsIn(x - g_window_manager->GetScreenOffsetX(), y);
}
bool IsPointInsideActiveUI(int x, int y)
{
if (!g_window_manager) return false;
return g_window_manager->IsPointInsideActiveUI(x, y);
return g_window_manager->IsPointInsideActiveUI(x - g_window_manager->GetScreenOffsetX(), y);
}
PlayerStatusInfo GetPlayerStatusInfo()
@@ -745,6 +863,32 @@ bool run_main_script_body(const char* lpCmdLine, std::string* error)
namespace PythonBoot
{
// PORT: the auto hunt window (AutoHuntScript.inc) as module mt_autohunt and the account registration
// window (RegisterScript.inc) as mt_register. They hook game.GameWindow / introLogin.LoginWindow, so they
// have to run once the root scripts are importable; a failure leaves the game without them, not broken.
#include "AutoHuntScript.inc"
#include "RegisterScript.inc"
static void install_script_module(const char* c_szModule, const char* c_szScript)
{
PyObject* module = PyImport_AddModule(c_szModule);
PyObject* dict = module ? PyModule_GetDict(module) : nullptr;
if (!dict)
{
PyErr_Clear();
return;
}
if (!PyDict_GetItemString(dict, "__builtins__"))
PyDict_SetItemString(dict, "__builtins__", PyEval_GetBuiltins());
PyObject* result = PyRun_String(c_szScript, Py_file_input, dict, dict);
if (!result)
{
TraceError("%s: install failed", c_szModule);
PyErr_Print();
}
Py_XDECREF(result);
}
bool RunMainScript(const char* lpCmdLine, std::string* error)
{
if (!g_launcher)
@@ -775,6 +919,11 @@ bool RunMainScript(const char* lpCmdLine, std::string* error)
if (!g_fiber.ok)
return fail(error, g_fiber.error);
}
if (IsAppLooping())
{
install_script_module("mt_autohunt", c_szAutoHuntScript);
install_script_module("mt_register", c_szRegisterScript);
}
return true;
}
+29 -4
View File
@@ -59,6 +59,10 @@ bool Evaluate(const char* expression, std::string* result, std::string* error);
// Forward physical Godot input to the original 40250 window manager. Coordinates
// are viewport pixels; SetUISize also establishes its logical resolution.
void SetUISize(int width, int height);
// Mobile safe area: the 40250 windows are laid out width - 2 * inset wide and drawn inset pixels
// right of the screen edge (clear of rounded corners and cutouts) while the 3D world keeps the whole
// screen. Input coordinates stay screen pixels. Takes effect at the next SetUISize; 0 = off.
void SetUISafeInset(int inset);
void UIMouseMove(int x, int y);
void UIMouseButton(int button, bool pressed, int x, int y);
void UIMouseWheel(int nLen);
@@ -68,11 +72,32 @@ void SetMoveDirection(float angleDeg, bool moving);
void SetAttackKey(bool pressed);
void UseLocalQuickSlot(int localSlotIndex);
bool TryAssignAttachedObjectToLocalQuickSlot(int localSlotIndex);
std::string LocalQuickSlotSkillIcon(int localSlotIndex);
void CameraBeginDrag(int x, int y);
void CameraDrag(int x, int y);
void CameraEndDrag();
// PORT: auto hunt (CPythonPlayer::AutoHunt_*, window in AutoHuntScript.inc): start/stop, the settings
// window, and whether it runs / a character is in the world to run it.
void AutoHuntToggle();
void AutoHuntOpenSettings();
bool AutoHuntIsEnabled();
bool AutoHuntAvailable();
// The skill or inventory item in the local quick slot: its UI texture name, the icon's (su, sv, eu, ev)
// inside it and the item count (0 for a skill).
bool LocalQuickSlotIcon(int localSlotIndex, std::string* texture, float uv[4], int* count);
// Touch gestures: a Win32 WM_LBUTTONDBLCLK (40250 CPythonApplication::OnMouseLeftButtonDoubleClick),
// whether an icon hangs on the cursor (CWindowManager::IsAttaching) and mouseController.DeattachObject().
void UIMouseDoubleClick(int x, int y);
bool UIIsAttaching();
void UIDeattachObject();
// Turns the camera around the player immediately, without CCamera::Drag's decaying velocity.
void CameraRotateBy(float rollDeg, float pitchDeg);
bool IsPointInsideActiveUI(int x, int y);
// Touch hosts: hide 40250's on-screen login keypad (the system keyboard types instead).
// Also drops the mouse arrow: touch hosts run 40250's hardware-cursor mode with no OS cursor, so
// mouseModule draws only the attached icon.
void SetTouchInput(bool touch);
bool TouchInput();
// An EditLine has focus (40250 ui.EditLine.OnSetFocus -> ime.EnableCaptureInput).
bool TextInputFocused();
// (x, y) hits the focused window, i.e. the tap landed on the focused EditLine.
bool IsPointInsideFocusedWindow(int x, int y);
struct PlayerStatusInfo {
int level = 1;
@@ -0,0 +1,328 @@
// The account registration window (PORT; 40250 has no counterpart, its accounts were made on the web),
// run by PythonBoot::RunMainScript as module mt_register. It hooks introLogin.LoginWindow.Open/Close and
// puts a register button under the login board; the dialog posts to the server's registration service
// (tools/40250/register_server.py) through net.RegisterAccount. Strings are UTF-8 here and go through
// player.AutoHuntLocalize into the locale's code page.
static const char c_szRegisterScript[] = R"PY(# -*- coding: utf-8 -*-
import introLogin
import net
import player
import ui
import wndMgr
L = player.AutoHuntLocalize
REGISTER_PORT = 11080
LOGIN_MIN, LOGIN_MAX = 4, 16
PASSWORD_MIN, PASSWORD_MAX = 4, 16
SOCIAL_ID_LEN = 7
LARGE_BUTTON = "d:/ymir work/ui/public/large_button_%02d.sub"
MIDDLE_BUTTON = "d:/ymir work/ui/public/middle_button_%02d.sub"
COLOR_ERROR = 0xffff7f7f
COLOR_INFO = 0xffc8c8c8
ERROR_TEXT = {
"BAD_LOGIN": "账号需为4-16位英文字母或数字",
"BAD_PASSWORD": "密码需为4-16位字母、数字或符号",
"BAD_SOCIAL_ID": "删除码需为7位数字",
"EXISTS": "该账号已存在",
"RATE_LIMIT": "注册过于频繁,请稍后再试",
"CONNECT": "无法连接注册服务器",
"TIMEOUT": "注册服务器无响应",
"SERVER": "注册服务器出错,请稍后再试",
}
def _IsAlnum(text):
for c in text:
if not (("a" <= c <= "z") or ("A" <= c <= "Z") or ("0" <= c <= "9")):
return False
return True
def _IsPrintable(text):
for c in text:
if not ("!" <= c <= "~"):
return False
return True
def _Validate(login, password, confirm, socialID):
if not (LOGIN_MIN <= len(login) <= LOGIN_MAX) or not _IsAlnum(login):
return ERROR_TEXT["BAD_LOGIN"]
if not (PASSWORD_MIN <= len(password) <= PASSWORD_MAX) or not _IsPrintable(password):
return ERROR_TEXT["BAD_PASSWORD"]
if password != confirm:
return "两次输入的密码不一致"
if len(socialID) != SOCIAL_ID_LEN or not socialID.isdigit():
return ERROR_TEXT["BAD_SOCIAL_ID"]
return None
def _MakeButton(parent, x, y, image, text, event):
button = ui.Button()
button.SetParent(parent)
button.SetPosition(x, y)
button.SetUpVisual(image % 1)
button.SetOverVisual(image % 2)
button.SetDownVisual(image % 3)
button.SetText(L(text))
button.SetEvent(event)
button.Show()
return button
class InputRow:
LABEL_X = 18
SLOT_X = 96
SLOT_WIDTH = 160
def __init__(self, parent, y, label, maxLen):
self.label = ui.TextLine()
self.label.SetParent(parent)
self.label.SetPosition(self.LABEL_X, y + 2)
self.label.SetText(L(label))
self.label.Show()
self.slot = ui.SlotBar()
self.slot.SetParent(parent)
self.slot.SetPosition(self.SLOT_X, y)
self.slot.SetSize(self.SLOT_WIDTH, 18)
self.slot.Show()
self.edit = ui.EditLine()
self.edit.SetParent(self.slot)
self.edit.SetPosition(4, 2)
self.edit.SetSize(self.SLOT_WIDTH - 8, 16)
self.edit.SetMax(maxLen)
self.edit.SetIMEFlag(0)
self.edit.Show()
def GetText(self):
return self.edit.GetText()
def Destroy(self):
self.edit.SetReturnEvent(0)
self.edit.SetTabEvent(0)
self.edit = None
self.slot = None
self.label = None
class RegisterDialog(ui.BoardWithTitleBar):
WIDTH = 280
HEIGHT = 222
def __init__(self, loginWindow):
ui.BoardWithTitleBar.__init__(self)
self.loginWindow = loginWindow
self.pending = False
self.pendingLogin = ""
self.pendingPassword = ""
self.AddFlag("movable")
self.AddFlag("float")
self.SetSize(self.WIDTH, self.HEIGHT)
self.SetTitleName(L("注册账号"))
self.SetCloseEvent(ui.__mem_func__(self.Close))
self.loginRow = InputRow(self, 38, "账号", LOGIN_MAX)
self.passwordRow = InputRow(self, 66, "密码", PASSWORD_MAX)
self.passwordRow.edit.SetSecret(True)
self.confirmRow = InputRow(self, 94, "确认密码", PASSWORD_MAX)
self.confirmRow.edit.SetSecret(True)
self.socialRow = InputRow(self, 122, "删除码", SOCIAL_ID_LEN)
self.socialRow.edit.SetNumberMode()
rows = (self.loginRow, self.passwordRow, self.confirmRow, self.socialRow)
for i in xrange(len(rows)):
nextEdit = rows[(i + 1) % len(rows)].edit
rows[i].edit.SetTabEvent(ui.__mem_func__(nextEdit.SetFocus))
if i + 1 < len(rows):
rows[i].edit.SetReturnEvent(ui.__mem_func__(nextEdit.SetFocus))
self.socialRow.edit.SetReturnEvent(ui.__mem_func__(self.__OnClickRegister))
for row in rows:
row.edit.SetEscapeEvent(ui.__mem_func__(self.Close))
self.hintLine = ui.TextLine()
self.hintLine.SetParent(self)
self.hintLine.SetPosition(0, 150)
self.hintLine.SetWindowHorizontalAlignCenter()
self.hintLine.SetHorizontalAlignCenter()
self.hintLine.Show()
self.registerButton = _MakeButton(self, self.WIDTH / 2 - 66, 180, MIDDLE_BUTTON, "注册",
ui.__mem_func__(self.__OnClickRegister))
self.cancelButton = _MakeButton(self, self.WIDTH / 2 + 5, 180, MIDDLE_BUTTON, "取消",
ui.__mem_func__(self.Close))
def Destroy(self):
self.Hide()
for row in (self.loginRow, self.passwordRow, self.confirmRow, self.socialRow):
row.Destroy()
self.loginRow = self.passwordRow = self.confirmRow = self.socialRow = None
self.registerButton = None
self.cancelButton = None
self.hintLine = None
self.loginWindow = None
def Open(self):
for row in (self.loginRow, self.passwordRow, self.confirmRow, self.socialRow):
row.edit.SetText("")
self.__SetHint("账号仅限英文字母和数字,删除码用于删除角色", COLOR_INFO)
self.SetPosition((wndMgr.GetScreenWidth() - self.WIDTH) / 2, (wndMgr.GetScreenHeight() - self.HEIGHT) / 2)
self.Show()
self.SetTop()
self.loginRow.edit.SetFocus()
def Close(self):
for row in (self.loginRow, self.passwordRow, self.confirmRow, self.socialRow):
row.edit.KillFocus()
self.Hide()
if self.loginWindow and self.loginWindow.idEditLine:
self.loginWindow.idEditLine.SetFocus()
return True
def OnPressEscapeKey(self):
return self.Close()
def __SetHint(self, text, color):
self.hintLine.SetText(L(text))
self.hintLine.SetPackedFontColor(color)
def __GetHost(self):
stream = self.loginWindow.stream
if stream.account_addr:
return stream.account_addr
return stream.addr
def __OnClickRegister(self):
if self.pending:
return
login = self.loginRow.GetText()
password = self.passwordRow.GetText()
error = _Validate(login, password, self.confirmRow.GetText(), self.socialRow.GetText())
if error:
self.__SetHint(error, COLOR_ERROR)
return
host = self.__GetHost()
if not host:
self.__SetHint("请先选择服务器", COLOR_ERROR)
return
if not net.RegisterAccount(host, REGISTER_PORT, login, password, self.socialRow.GetText()):
return
self.pending = True
self.pendingLogin = login.lower()
self.pendingPassword = password
self.registerButton.Disable()
self.__SetHint("正在注册...", COLOR_INFO)
def OnUpdate(self):
if not self.pending:
return
result = net.GetRegisterAccountResult()
if not result:
return
self.pending = False
self.registerButton.Enable()
if result == "OK":
self.__OnRegistered()
return
code = result[4:] if result.startswith("ERR ") else "SERVER"
self.__SetHint(ERROR_TEXT.get(code, ERROR_TEXT["SERVER"]), COLOR_ERROR)
def __OnRegistered(self):
loginWindow = self.loginWindow
login, password = self.pendingLogin, self.pendingPassword
self.pendingPassword = ""
self.Close()
if loginWindow.idEditLine:
loginWindow.idEditLine.SetText(login)
if loginWindow.pwdEditLine:
loginWindow.pwdEditLine.SetText(password)
loginWindow.PopupNotifyMessage(L("账号 %s 注册成功,请点击登录") % login)
class RegisterEntry(ui.Window):
"The button under the login board; it follows the board's position and visibility every frame."
def __init__(self, loginWindow):
ui.Window.__init__(self)
self.loginWindow = loginWindow
self.dialog = RegisterDialog(loginWindow)
self.button = ui.Button()
self.button.SetUpVisual(LARGE_BUTTON % 1)
self.button.SetOverVisual(LARGE_BUTTON % 2)
self.button.SetDownVisual(LARGE_BUTTON % 3)
self.button.SetText(L("注册账号"))
self.button.SetEvent(ui.__mem_func__(self.dialog.Open))
self.SetSize(0, 0)
self.Show()
def Destroy(self):
self.Hide()
self.button.Hide()
self.button = None
self.dialog.Destroy()
self.dialog = None
self.loginWindow = None
def OnUpdate(self):
board = self.loginWindow.loginBoard if self.loginWindow else None
if not board or not self.loginWindow.IsShow() or not board.IsShow() or self.dialog.IsShow():
if self.button.IsShow():
self.button.Hide()
return
x, y = board.GetGlobalPosition()
self.button.SetPosition(x + (board.GetWidth() - self.button.GetWidth()) / 2, y + board.GetHeight() + 6)
if not self.button.IsShow():
self.button.Show()
self.button.SetTop()
_entries = {}
def Attach(loginWindow):
key = id(loginWindow)
if key not in _entries:
_entries[key] = RegisterEntry(loginWindow)
def Detach(loginWindow):
entry = _entries.pop(id(loginWindow), None)
if entry:
entry.Destroy()
def __Install():
openLogin = introLogin.LoginWindow.Open
closeLogin = introLogin.LoginWindow.Close
def Open(self):
result = openLogin(self)
try:
Attach(self)
except:
import dbg
dbg.TraceError("mt_register: attach failed")
return result
def Close(self):
Detach(self)
return closeLogin(self)
introLogin.LoginWindow.Open = Open
introLogin.LoginWindow.Close = Close
# system.py reaches app.Loop() before this runs, so a login window may already be open.
import gc
for obj in gc.get_objects():
if isinstance(obj, introLogin.LoginWindow) and getattr(obj, "loginBoard", None):
Attach(obj)
__Install()
)PY";
@@ -176,7 +176,7 @@ bool CPythonApplication::Create(PyObject*, const char*, int, int, int)
if (IsNativeTerrainRenderEnabled())
{
const bool use_hardware_cursor = PythonBoot::HostHardwareCursorEnabled();
const bool use_hardware_cursor = PythonBoot::HostHardwareCursorEnabled() || PythonBoot::TouchInput();
if (CPythonSystem::InstancePtr() && CPythonSystem::Instance().GetConfig())
CPythonSystem::Instance().GetConfig()->is_software_cursor = !use_hardware_cursor;
SetCursorMode(use_hardware_cursor ? CURSOR_MODE_HARDWARE : CURSOR_MODE_SOFTWARE);
@@ -214,6 +214,8 @@ void CPythonApplication::RenderGame()
{
long lx, ly;
UI::CWindowManager::Instance().GetMousePosition(lx, ly);
// PORT: window coordinates -> the screen coordinates of the full-width 3D view.
lx += UI::CWindowManager::Instance().GetScreenOffsetX();
CPythonGraphic::Instance().SetCursorPosition(lx, ly);
}
@@ -453,7 +455,8 @@ int s_iCursorMode = CPythonApplication::CURSOR_MODE_HARDWARE; // m_iCurs
void CPythonApplication::SetCursorVisible(BOOL bFlag, bool bLiarCursorOn)
{
s_bCursorVisible = bFlag;
s_bLiarCursorOn = bLiarCursorOn;
// PORT: a touch screen has no cursor, not even the stand-in 40250 draws while the real one is hidden.
s_bLiarCursorOn = bLiarCursorOn && !PythonBoot::TouchInput();
// PORT: 40250 also counts ShowCursor up or down in hardware mode; the host has no OS cursor to hide.
}
+4
View File
@@ -92,6 +92,10 @@ target_include_directories(port_logic PRIVATE ${MT_PORT_GENERATED})
if(WIN32)
target_link_libraries(port_logic PUBLIC winmm)
endif()
if(ANDROID)
# PythonPlayerAutoHunt.cpp traces to logcat.
target_link_libraries(port_logic PUBLIC log)
endif()
# 40250 Distribute|Win32 preprocessor definitions per library (vs_files/<Lib>/<Lib>.vcxproj),
# minus WIN32/_WINDOWS/_LIB/_CRT_SECURE_NO_WARNINGS: Windows-only branches go through platform/,
@@ -85,8 +85,17 @@ namespace UI
m_poHandler = NULL;
}
static bool s_bHideVirtualKeyboard = false;
void CWindow::SetHideVirtualKeyboard(bool bHide)
{
s_bHideVirtualKeyboard = bHide;
}
void CWindow::Show()
{
if (s_bHideVirtualKeyboard && m_strName == "VirtualKeyboard")
return;
m_bShow = true;
}
@@ -12,6 +12,10 @@ namespace UI
static DWORD Type();
BOOL IsType(DWORD dwType);
// PORT: touch hosts type with the system keyboard, so windows named "VirtualKeyboard"
// (intrologin.py's on-screen keypad) never show.
static void SetHideVirtualKeyboard(bool bHide);
enum EHorizontalAlign
{
HORIZONTAL_ALIGN_LEFT = 0,
@@ -4,6 +4,9 @@
#include "PythonGridSlotWindow.h"
#include "PythonWindowManager.h"
// PORT: platform EterLib/UIRenderCommands.cpp (the UI command recorder); see SetScreenOffsetX.
void UIRenderSetOffsetX(float x);
//#define __WINDOW_LEAK_CHECK__
BOOL g_bShowOverInWindowName = FALSE;
@@ -700,7 +703,18 @@ namespace UI
void CWindowManager::Render()
{
m_pRootWindow->Render();
if (!m_lScreenOffsetX)
{
m_pRootWindow->Render();
return;
}
// PORT: the root has no content of its own; its children are the layers, in this order.
for (CWindow * pLayer : m_LayerWindowList)
{
UIRenderSetOffsetX(0 == strcmp(pLayer->GetName(), "GAME") ? 0.0f : float(m_lScreenOffsetX));
pLayer->Render();
}
UIRenderSetOffsetX(0.0f);
}
CWindow * CWindowManager::__PickWindow(long x, long y)
@@ -728,6 +742,16 @@ namespace UI
}
}
// PORT: the GAME layer's window is laid out GetScreenWidth() wide but its world fills the
// screen; the strips beside the shifted layers still belong to it.
if (m_lScreenOffsetX && (x < 0 || x >= m_lWidth))
{
CWindow * pGameLayer = m_LayerWindowMap["GAME"];
CWindow * pPickedWindow = pGameLayer->PickWindow(x < 0 ? 0 : m_lWidth - 1, y);
if (pPickedWindow != pGameLayer)
return pPickedWindow;
}
return NULL;
}
@@ -757,7 +781,12 @@ namespace UI
if (m_iVres==0)
return;
m_lMouseX = m_lWidth * x / m_iHres;
// PORT: x is a screen position; the windows live m_lScreenOffsetX to the right of it.
const long lScreenWidth = m_iHres - 2 * m_lScreenOffsetX;
if (m_lScreenOffsetX && lScreenWidth > 0)
m_lMouseX = m_lWidth * (x - m_lScreenOffsetX) / lScreenWidth;
else
m_lMouseX = m_lWidth * x / m_iHres;
m_lMouseY = m_lHeight * y / m_iVres;
}
@@ -29,6 +29,12 @@ namespace UI
void SetMouseHandler(PyObject * poMouseHandler);
long GetScreenWidth() { return m_lWidth; }
// PORT (mobile safe area): the window layers other than GAME sit this many pixels right of
// the screen's left edge, inside a resolution m_iHres = m_lWidth + 2 * offset wide. The GAME
// layer (3D world, text tails) keeps screen coordinates; window/mouse coordinates are the
// shifted ones. 0 is the verbatim 40250 layout.
void SetScreenOffsetX(long lOffsetX) { m_lScreenOffsetX = lOffsetX; }
long GetScreenOffsetX() { return m_lScreenOffsetX; }
long GetScreenHeight() { return m_lHeight; }
void GetMousePosition(long & rx, long & ry);
BOOL IsDragging();
@@ -151,6 +157,7 @@ namespace UI
int m_iVres;
int m_iHres;
long m_lScreenOffsetX = 0;
long m_lMouseX, m_lMouseY;
long m_lDragX, m_lDragY;
@@ -0,0 +1,198 @@
// PORT: in-client account registration. 40250 has no counterpart: its accounts were made on the web.
//
// net.RegisterAccount(host, port, login, password, socialId) starts one HTTP POST /register to the
// server's registration service (tools/40250/register_server.py) on a worker thread;
// net.GetRegisterAccountResult() polls it from the login window's OnUpdate. The reply is a single
// line, "OK" or "ERR <CODE>", returned to Python as-is.
#include "StdAfx.h"
#include <atomic>
#include <cerrno>
#include <cstring>
#include <mutex>
#include <string>
#include <thread>
#include <arpa/inet.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <poll.h>
#include <sys/socket.h>
#include <unistd.h>
namespace
{
enum ERegisterState
{
REGISTER_IDLE,
REGISTER_PENDING,
REGISTER_DONE,
};
const int c_iRegisterTimeoutMs = 8000;
std::mutex gs_kRegisterMutex;
std::atomic<int> gs_iRegisterState(REGISTER_IDLE);
std::string gs_strRegisterResult;
std::string __UrlEncode(const std::string& c_rstText)
{
static const char c_szHex[] = "0123456789ABCDEF";
std::string stOut;
for (unsigned char c : c_rstText)
{
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~')
{
stOut += char(c);
}
else
{
stOut += '%';
stOut += c_szHex[c >> 4];
stOut += c_szHex[c & 15];
}
}
return stOut;
}
bool __WaitFor(int iSocket, short sEvents)
{
pollfd kPoll = { iSocket, sEvents, 0 };
return poll(&kPoll, 1, c_iRegisterTimeoutMs) == 1 && (kPoll.revents & sEvents);
}
std::string __Post(const std::string& c_rstHost, int iPort, const std::string& c_rstBody)
{
addrinfo kHints;
memset(&kHints, 0, sizeof(kHints));
kHints.ai_family = AF_INET;
kHints.ai_socktype = SOCK_STREAM;
addrinfo* pkAddr = NULL;
if (getaddrinfo(c_rstHost.c_str(), std::to_string(iPort).c_str(), &kHints, &pkAddr) != 0 || !pkAddr)
return "ERR CONNECT";
const int iSocket = socket(pkAddr->ai_family, pkAddr->ai_socktype, pkAddr->ai_protocol);
if (iSocket < 0)
{
freeaddrinfo(pkAddr);
return "ERR CONNECT";
}
#ifdef SO_NOSIGPIPE
const int iOne = 1;
setsockopt(iSocket, SOL_SOCKET, SO_NOSIGPIPE, &iOne, sizeof(iOne));
#endif
fcntl(iSocket, F_SETFL, fcntl(iSocket, F_GETFL, 0) | O_NONBLOCK);
std::string stResult = "ERR CONNECT";
int iError = 0;
socklen_t iErrorLen = sizeof(iError);
if (connect(iSocket, pkAddr->ai_addr, pkAddr->ai_addrlen) == 0 ||
(errno == EINPROGRESS && __WaitFor(iSocket, POLLOUT) &&
getsockopt(iSocket, SOL_SOCKET, SO_ERROR, &iError, &iErrorLen) == 0 && iError == 0))
{
const std::string stRequest =
"POST /register HTTP/1.0\r\n"
"Host: " + c_rstHost + "\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: " + std::to_string(c_rstBody.size()) + "\r\n"
"Connection: close\r\n\r\n" + c_rstBody;
size_t uSent = 0;
while (uSent < stRequest.size() && __WaitFor(iSocket, POLLOUT))
{
#ifdef MSG_NOSIGNAL
const ssize_t n = send(iSocket, stRequest.data() + uSent, stRequest.size() - uSent, MSG_NOSIGNAL);
#else
const ssize_t n = send(iSocket, stRequest.data() + uSent, stRequest.size() - uSent, 0);
#endif
if (n <= 0)
break;
uSent += size_t(n);
}
std::string stResponse;
if (uSent == stRequest.size())
{
char acBuf[512];
while (stResponse.size() < 4096 && __WaitFor(iSocket, POLLIN))
{
const ssize_t n = recv(iSocket, acBuf, sizeof(acBuf), 0);
if (n <= 0)
break;
stResponse.append(acBuf, size_t(n));
}
}
const size_t uBody = stResponse.find("\r\n\r\n");
if (uBody == std::string::npos)
{
stResult = uSent == stRequest.size() ? "ERR TIMEOUT" : "ERR CONNECT";
}
else
{
stResult = stResponse.substr(uBody + 4);
const size_t uEnd = stResult.find_first_of("\r\n");
if (uEnd != std::string::npos)
stResult.resize(uEnd);
if (stResult.empty())
stResult = "ERR SERVER";
}
}
close(iSocket);
freeaddrinfo(pkAddr);
return stResult;
}
}
PyObject* netRegisterAccount(PyObject* poSelf, PyObject* poArgs)
{
char* szHost;
int iPort;
char* szLogin;
char* szPassword;
char* szSocialID;
if (!PyTuple_GetString(poArgs, 0, &szHost))
return Py_BuildException();
if (!PyTuple_GetInteger(poArgs, 1, &iPort))
return Py_BuildException();
if (!PyTuple_GetString(poArgs, 2, &szLogin))
return Py_BuildException();
if (!PyTuple_GetString(poArgs, 3, &szPassword))
return Py_BuildException();
if (!PyTuple_GetString(poArgs, 4, &szSocialID))
return Py_BuildException();
int iExpected = REGISTER_IDLE;
if (!gs_iRegisterState.compare_exchange_strong(iExpected, REGISTER_PENDING))
{
iExpected = REGISTER_DONE;
if (!gs_iRegisterState.compare_exchange_strong(iExpected, REGISTER_PENDING))
return Py_BuildValue("i", 0); // one request at a time
}
const std::string stHost = szHost;
const std::string stBody = "login=" + __UrlEncode(szLogin) + "&password=" + __UrlEncode(szPassword) +
"&social_id=" + __UrlEncode(szSocialID);
std::thread([stHost, iPort, stBody]()
{
const std::string stResult = __Post(stHost, iPort, stBody);
{
std::lock_guard<std::mutex> kLock(gs_kRegisterMutex);
gs_strRegisterResult = stResult;
}
gs_iRegisterState = REGISTER_DONE;
}).detach();
return Py_BuildValue("i", 1);
}
// Returns "" while the request runs (or none was started), else its result once.
PyObject* netGetRegisterAccountResult(PyObject* poSelf, PyObject* poArgs)
{
int iExpected = REGISTER_DONE;
if (!gs_iRegisterState.compare_exchange_strong(iExpected, REGISTER_IDLE))
return Py_BuildValue("s", "");
std::lock_guard<std::mutex> kLock(gs_kRegisterMutex);
return Py_BuildValue("s", gs_strRegisterResult.c_str());
}
@@ -672,6 +672,13 @@ BOOL CPythonItem::GetGroundItemPosition(DWORD dwVirtualID, TPixelPosition * pPos
return TRUE;
}
void CPythonItem::GetGroundItemVIDs(std::vector<DWORD>* pkVecVID)
{
pkVecVID->clear();
for (TGroundItemInstanceMap::iterator itor = m_GroundItemInstanceMap.begin(); itor != m_GroundItemInstanceMap.end(); ++itor)
pkVecVID->push_back(itor->first);
}
DWORD CPythonItem::__Pick(const POINT& c_rkPtMouse)
{
float fu, fv, ft;
@@ -109,6 +109,8 @@ class CPythonItem : public CSingleton<CPythonItem>
bool GetCloseMoney(const TPixelPosition & c_rPixelPosition, DWORD* dwItemID, DWORD dwDistance=300);
DWORD GetVirtualNumberOfGroundItem(DWORD dwVID);
// PORT: auto hunt (CPythonPlayer::__AutoHunt_FindItem) walks the ground items.
void GetGroundItemVIDs(std::vector<DWORD>* pkVecVID);
void BuildNoGradeNameData(int iType);
DWORD GetNoGradeNameDataCount();
@@ -11,6 +11,10 @@ static std::string gs_stServerInfo;
extern BOOL gs_bEmpireLanuageEnable;
std::list<std::string> g_kList_strCommand;
// PORT: PythonAccountRegister.cpp
PyObject* netRegisterAccount(PyObject* poSelf, PyObject* poArgs);
PyObject* netGetRegisterAccountResult(PyObject* poSelf, PyObject* poArgs);
PyObject* netGetBettingGuildWarValue(PyObject* poSelf, PyObject* poArgs)
{
char* szName;
@@ -1769,6 +1773,8 @@ void initnet()
{ "SendSelectCharacterPacket", netSendSelectCharacterPacket, METH_VARARGS },
{ "SendChangeNamePacket", netSendChangeNamePacket, METH_VARARGS },
{ "SendCreateCharacterPacket", netSendCreateCharacterPacket, METH_VARARGS },
{ "RegisterAccount", netRegisterAccount, METH_VARARGS },
{ "GetRegisterAccountResult", netGetRegisterAccountResult, METH_VARARGS },
{ "SendDestroyCharacterPacket", netSendDestroyCharacterPacket, METH_VARARGS },
{ "SendEnterGamePacket", netSendEnterGamePacket, METH_VARARGS },
@@ -176,6 +176,7 @@ void CPythonPlayer::Update()
PyCallClassMemberFunc(m_ppyGameWindow, "RefreshStamina", Py_BuildValue("()"));
}
__Update_AutoHunt(); // PORT: before __Update_AutoAttack, which acts on the target it sets
__Update_AutoAttack();
__Update_NotifyGuildAreaEvent();
}
@@ -525,6 +526,9 @@ void CPythonPlayer::NotifyCharacterUpdate(DWORD dwVID)
void CPythonPlayer::NotifyDeadMainCharacter()
{
__ClearAutoAttackTargetActorID();
// PORT: auto hunt stops with the character.
if (AutoHunt_IsEnabled())
__AutoHunt_Stop(AUTO_HUNT_STOP_DEAD);
}
void CPythonPlayer::NotifyChangePKMode()
@@ -1698,6 +1702,12 @@ void CPythonPlayer::Clear()
m_bMobileFlag = FALSE;
// PORT: leaving the game (logout, character select) ends auto hunt without a stop notice.
m_eAutoHuntState = AUTO_HUNT_OFF;
m_eAutoHuntStopReason = AUTO_HUNT_STOP_NONE;
m_dwAutoHuntTargetVID = 0;
m_dwAutoHuntItemID = 0;
__ClearAutoAttackTargetActorID();
}
@@ -1731,6 +1741,34 @@ CPythonPlayer::CPythonPlayer(void)
m_sysIsLevelLimit = TRUE;
m_dwPlayTime = 0;
// PORT: auto hunt.
m_isAutoHuntConfigLoaded = false;
m_eAutoHuntState = AUTO_HUNT_OFF;
m_eAutoHuntStopReason = AUTO_HUNT_STOP_NONE;
m_kAutoHuntConfig.iRange = 2000;
m_kAutoHuntConfig.iHPPercent = 40;
m_kAutoHuntConfig.iSPPercent = 20;
m_kAutoHuntConfig.iPickup = AUTO_HUNT_PICKUP_ALL;
m_kAutoHuntConfig.bAttackStone = false;
m_kAutoHuntConfig.bAttackBoss = false;
m_kAutoHuntConfig.dwSkillSlotMask = (1 << AUTO_HUNT_SKILL_SLOT_NUM) - 1;
m_kAutoHuntConfig.iGatherCount = 0;
m_isAutoHuntGathering = false;
m_fAutoHuntGatherStartTime = 0.0f;
m_fAutoHuntHitTime = 0.0f;
m_fAutoHuntLastUpdateTime = 0.0f;
m_fAutoHuntResumeTime = 0.0f;
m_dwAutoHuntTargetVID = 0;
m_fAutoHuntStateTime = 0.0f;
m_fAutoHuntProgressTime = 0.0f;
m_fAutoHuntBestDistance = 0.0f;
m_fAutoHuntNextActionTime = 0.0f;
m_fAutoHuntNextSkillTime = 0.0f;
m_fAutoHuntNextPotionTime = 0.0f;
m_dwAutoHuntItemID = 0;
m_isAutoHuntNoHPPotion = false;
m_isAutoHuntNoSPPotion = false;
m_aeMBFButton[MBT_LEFT]=CPythonPlayer::MBF_SMART;
m_aeMBFButton[MBT_RIGHT]=CPythonPlayer::MBF_CAMERA;
m_aeMBFButton[MBT_MIDDLE]=CPythonPlayer::MBF_CAMERA;
+102 -1
View File
@@ -222,7 +222,8 @@ class CPythonPlayer : public CSingleton<CPythonPlayer>, public IAbstractPlayer
bool NEW_MoveToDestPixelPositionDirection(const TPixelPosition& c_rkPPosDst);
bool NEW_MoveToMousePickedDirection();
bool NEW_MoveToMouseScreenDirection();
bool NEW_MoveToDirection(float fDirRot);
// PORT: isAutoCameraRotation=false skips 40250's keyboard camera roll (mobile joystick).
bool NEW_MoveToDirection(float fDirRot, bool isAutoCameraRotation = true);
void NEW_Stop();
@@ -674,6 +675,106 @@ class CPythonPlayer : public CSingleton<CPythonPlayer>, public IAbstractPlayer
protected:
float MOVABLE_GROUND_DISTANCE;
// PORT: auto hunt (PythonPlayerAutoHunt.cpp). 40250 has no counterpart; it only drives the existing
// player paths (auto attack, __UseSkill, item reserve), so the server sees ordinary play.
public:
enum EAutoHuntState
{
AUTO_HUNT_OFF,
AUTO_HUNT_SEARCH,
AUTO_HUNT_ENGAGE,
AUTO_HUNT_LOOT,
AUTO_HUNT_RETURN,
AUTO_HUNT_PAUSED,
};
enum EAutoHuntStopReason
{
AUTO_HUNT_STOP_NONE,
AUTO_HUNT_STOP_USER,
AUTO_HUNT_STOP_DEAD,
AUTO_HUNT_STOP_MAP,
AUTO_HUNT_STOP_INVENTORY_FULL,
};
enum EAutoHuntPickup
{
AUTO_HUNT_PICKUP_NONE,
AUTO_HUNT_PICKUP_ALL,
AUTO_HUNT_PICKUP_GOLD_EQUIP,
};
enum
{
AUTO_HUNT_SKILL_SLOT_NUM = 8, // the local quick slots on the taskbar's first page
};
struct SAutoHuntConfig
{
int iRange; // cm (100 = 1 m) around the anchor
int iHPPercent; // drink an HP potion below this, 0 = never
int iSPPercent;
int iPickup; // EAutoHuntPickup
bool bAttackStone;
bool bAttackBoss;
DWORD dwSkillSlotMask; // bit i = local quick slot i may be cast
int iGatherCount; // gather mode: hit this many mobs once each, then fight them together; 0 = off
};
void AutoHunt_SetEnabled(bool isEnable);
bool AutoHunt_IsEnabled();
int AutoHunt_GetState();
int AutoHunt_GetStopReason();
// Gather mode: still pulling (vs. fighting the pack), and the pulled mobs still alive.
bool AutoHunt_IsGathering();
int AutoHunt_GetGatheredCount();
bool AutoHunt_HasNoPotion(bool isHP);
const SAutoHuntConfig& AutoHunt_GetConfig();
void AutoHunt_SetConfig(const SAutoHuntConfig& c_rkConfig);
protected:
void __AutoHunt_OnManualInput();
void __AutoHunt_Stop(int eReason);
void __Update_AutoHunt();
void __AutoHunt_UpdatePotion(CInstanceBase& rkInstMain, float fNow);
bool __AutoHunt_UpdateBuff(CInstanceBase& rkInstMain, float fNow);
void __AutoHunt_UpdateAttackSkill(CInstanceBase& rkInstMain, CInstanceBase& rkInstVictim, float fNow);
bool __AutoHunt_IsSkillReady(DWORD dwSkillSlot, CPythonSkill::TSkillData** ppSkillData);
DWORD __AutoHunt_FindTarget(CInstanceBase& rkInstMain, float fNow, bool isExceptGathered = false);
DWORD __AutoHunt_FindGatheredTarget(CInstanceBase& rkInstMain);
void __AutoHunt_PurgeGathered();
bool __AutoHunt_IsGatherDone(float fNow);
DWORD __AutoHunt_FindItem(CInstanceBase& rkInstMain, float fNow);
bool __AutoHunt_IsInventoryFull();
void __AutoHunt_Engage(DWORD dwVID, float fNow);
void __AutoHunt_LoadConfig();
void __AutoHunt_SaveConfig();
SAutoHuntConfig m_kAutoHuntConfig;
bool m_isAutoHuntConfigLoaded;
int m_eAutoHuntState;
int m_eAutoHuntStopReason;
TPixelPosition m_kPPosAutoHuntAnchor;
std::string m_stAutoHuntMapName;
float m_fAutoHuntResumeTime;
DWORD m_dwAutoHuntTargetVID;
float m_fAutoHuntStateTime; // when the current target / item / state began
float m_fAutoHuntProgressTime; // last time the target got closer or was hit
float m_fAutoHuntBestDistance;
float m_fAutoHuntNextActionTime;
float m_fAutoHuntNextSkillTime;
float m_fAutoHuntNextPotionTime;
DWORD m_dwAutoHuntItemID;
bool m_isAutoHuntNoHPPotion;
bool m_isAutoHuntNoSPPotion;
std::map<DWORD, float> m_kMap_dwAutoHuntSkipVID; // actor vid -> ignored until
std::map<DWORD, float> m_kMap_dwAutoHuntSkipItem; // ground item vid -> ignored until
bool m_isAutoHuntGathering;
float m_fAutoHuntGatherStartTime;
float m_fAutoHuntHitTime; // attacking the current target this long (gather tagging)
float m_fAutoHuntLastUpdateTime;
std::set<DWORD> m_kSet_dwAutoHuntGathered; // mobs hit while gathering, until they die
private:
std::map<DWORD, DWORD> m_kMap_dwAffectIndexToSkillIndex;
};
@@ -0,0 +1,875 @@
// PORT: auto hunt. 40250 has no counterpart; see PythonPlayer.h.
//
// Everything here goes through the paths a player's own input takes: the auto-attack target
// (__SetAutoAttackTargetActorID -> __Update_AutoAttack -> MODE_CLICK_ACTOR walks up and attacks),
// __UseSkill for the quick-slot skills, the MODE_CLICK_ITEM reserve that walks to a ground item and
// sends the pick-up, and CG_ITEM_USE for potions. No packet is sent that manual play would not send.
#include "StdAfx.h"
#include "PythonPlayer.h"
#include "PythonItem.h"
#include "PythonNonPlayer.h"
#include "PythonBackground.h"
#include "PythonCharacterManager.h"
#include "PythonNetworkStream.h"
#include "InstanceBase.h"
#include "../GameLib/ItemManager.h"
#include "../EterBase/Timer.h"
#include <cmath>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <vector>
#ifdef __ANDROID__
#include <android/log.h>
#endif
namespace
{
const char c_szAutoHuntConfigFileName[] = "autohunt.cfg";
const float c_fAutoHuntResumeDelay = 3.0f; // after the player's own input
const float c_fAutoHuntNoProgressTime = 4.0f; // neither closer to the target nor hitting it
const float c_fAutoHuntTargetTimeout = 60.0f;
const float c_fAutoHuntSkipTime = 30.0f; // an unreachable target is left alone this long
const float c_fAutoHuntLootDelay = 0.6f; // drops land a moment after the kill
const float c_fAutoHuntLootTimeout = 15.0f;
const float c_fAutoHuntItemTimeout = 5.0f; // one ground item (someone else's, blocked path)
const float c_fAutoHuntItemSkipTime = 60.0f;
const float c_fAutoHuntItemRange = 1500.0f;
const float c_fAutoHuntReturnDistance = 300.0f;
const float c_fAutoHuntReturnTimeout = 20.0f;
const float c_fAutoHuntSkillDistance = 400.0f; // melee skills are cast once this close
const BYTE c_byMobRankBoss = 4; // PAWN, S_PAWN, KNIGHT, S_KNIGHT, BOSS, KING
const DWORD c_dwGoldVnum = 1;
const float c_fAutoHuntTagHitTime = 0.35f; // gather: attacking a mob this long has pulled it
const float c_fAutoHuntGatherTimeout = 25.0f;
const float c_fAutoHuntGatherRange = 1500.0f; // gather: the next mob this close to the player
const int c_iAutoHuntGatherHPPercent = 50; // gather: stop pulling below this
// Trace: stderr with MT_AUTOHUNT_TRACE=1, logcat (tag mt_autohunt) on Android.
void AutoHuntTrace(const char* c_szFormat, ...)
{
static const bool s_isTrace = getenv("MT_AUTOHUNT_TRACE") != NULL;
va_list args;
#ifdef __ANDROID__
va_start(args, c_szFormat);
__android_log_vprint(ANDROID_LOG_INFO, "mt_autohunt", c_szFormat, args);
va_end(args);
#endif
if (!s_isTrace)
return;
va_start(args, c_szFormat);
fprintf(stderr, "autohunt: ");
vfprintf(stderr, c_szFormat, args);
fprintf(stderr, "\n");
va_end(args);
}
float PixelDistance(const TPixelPosition& a, const TPixelPosition& b)
{
const float dx = a.x - b.x;
const float dy = a.y - b.y;
return sqrtf(dx * dx + dy * dy);
}
}
void CPythonPlayer::AutoHunt_SetEnabled(bool isEnable)
{
if (!isEnable)
{
if (AutoHunt_IsEnabled())
__AutoHunt_Stop(AUTO_HUNT_STOP_USER);
return;
}
CInstanceBase* pkInstMain = NEW_GetMainActorPtr();
if (!pkInstMain || pkInstMain->IsDead())
return;
__AutoHunt_LoadConfig();
pkInstMain->NEW_GetPixelPosition(&m_kPPosAutoHuntAnchor);
m_stAutoHuntMapName = CPythonBackground::Instance().GetWarpMapName();
m_eAutoHuntState = AUTO_HUNT_SEARCH;
m_eAutoHuntStopReason = AUTO_HUNT_STOP_NONE;
m_dwAutoHuntTargetVID = 0;
m_dwAutoHuntItemID = 0;
m_fAutoHuntStateTime = CTimer::Instance().GetCurrentSecond();
m_fAutoHuntNextActionTime = 0.0f;
m_fAutoHuntNextSkillTime = 0.0f;
m_fAutoHuntNextPotionTime = 0.0f;
m_isAutoHuntNoHPPotion = false;
m_isAutoHuntNoSPPotion = false;
m_kMap_dwAutoHuntSkipVID.clear();
m_kMap_dwAutoHuntSkipItem.clear();
m_kSet_dwAutoHuntGathered.clear();
m_isAutoHuntGathering = false;
m_fAutoHuntHitTime = 0.0f;
m_fAutoHuntLastUpdateTime = m_fAutoHuntStateTime;
}
bool CPythonPlayer::AutoHunt_IsEnabled()
{
return AUTO_HUNT_OFF != m_eAutoHuntState;
}
int CPythonPlayer::AutoHunt_GetState()
{
return m_eAutoHuntState;
}
int CPythonPlayer::AutoHunt_GetStopReason()
{
return m_eAutoHuntStopReason;
}
bool CPythonPlayer::AutoHunt_IsGathering()
{
return AutoHunt_IsEnabled() && m_isAutoHuntGathering;
}
int CPythonPlayer::AutoHunt_GetGatheredCount()
{
int iCount = 0;
for (DWORD dwVID : m_kSet_dwAutoHuntGathered)
{
CInstanceBase* pkInst = NEW_FindActorPtr(dwVID);
if (pkInst && !pkInst->IsDead())
++iCount;
}
return iCount;
}
bool CPythonPlayer::AutoHunt_HasNoPotion(bool isHP)
{
return isHP ? m_isAutoHuntNoHPPotion : m_isAutoHuntNoSPPotion;
}
const CPythonPlayer::SAutoHuntConfig& CPythonPlayer::AutoHunt_GetConfig()
{
__AutoHunt_LoadConfig();
return m_kAutoHuntConfig;
}
void CPythonPlayer::AutoHunt_SetConfig(const SAutoHuntConfig& c_rkConfig)
{
m_isAutoHuntConfigLoaded = true;
m_kAutoHuntConfig = c_rkConfig;
m_kAutoHuntConfig.iRange = MINMAX(500, m_kAutoHuntConfig.iRange, 5000);
m_kAutoHuntConfig.iHPPercent = MINMAX(0, m_kAutoHuntConfig.iHPPercent, 95);
m_kAutoHuntConfig.iSPPercent = MINMAX(0, m_kAutoHuntConfig.iSPPercent, 95);
m_kAutoHuntConfig.iPickup = MINMAX(0, m_kAutoHuntConfig.iPickup, int(AUTO_HUNT_PICKUP_GOLD_EQUIP));
m_kAutoHuntConfig.dwSkillSlotMask &= (1 << AUTO_HUNT_SKILL_SLOT_NUM) - 1;
m_kAutoHuntConfig.iGatherCount = MINMAX(0, m_kAutoHuntConfig.iGatherCount, 10);
__AutoHunt_SaveConfig();
}
void CPythonPlayer::__AutoHunt_LoadConfig()
{
if (m_isAutoHuntConfigLoaded)
return;
m_isAutoHuntConfigLoaded = true;
FILE* fp = fopen(c_szAutoHuntConfigFileName, "rt");
if (!fp)
return;
SAutoHuntConfig kConfig = m_kAutoHuntConfig;
int iStone = 0, iBoss = 0, iGather = 0;
unsigned int uMask = 0;
// 7 fields before gather mode.
const int iRead = fscanf(fp, "%d %d %d %d %d %d %u %d", &kConfig.iRange, &kConfig.iHPPercent, &kConfig.iSPPercent,
&kConfig.iPickup, &iStone, &iBoss, &uMask, &iGather);
if (iRead >= 7)
{
kConfig.iGatherCount = 8 == iRead ? iGather : 0;
kConfig.bAttackStone = iStone != 0;
kConfig.bAttackBoss = iBoss != 0;
kConfig.dwSkillSlotMask = uMask;
fclose(fp);
AutoHunt_SetConfig(kConfig);
return;
}
fclose(fp);
}
void CPythonPlayer::__AutoHunt_SaveConfig()
{
FILE* fp = fopen(c_szAutoHuntConfigFileName, "wt");
if (!fp)
return;
fprintf(fp, "%d %d %d %d %d %d %u %d\n", m_kAutoHuntConfig.iRange, m_kAutoHuntConfig.iHPPercent,
m_kAutoHuntConfig.iSPPercent, m_kAutoHuntConfig.iPickup, m_kAutoHuntConfig.bAttackStone ? 1 : 0,
m_kAutoHuntConfig.bAttackBoss ? 1 : 0, (unsigned int)m_kAutoHuntConfig.dwSkillSlotMask,
m_kAutoHuntConfig.iGatherCount);
fclose(fp);
}
void CPythonPlayer::__AutoHunt_Stop(int eReason)
{
if (AUTO_HUNT_ENGAGE == m_eAutoHuntState || AUTO_HUNT_LOOT == m_eAutoHuntState || AUTO_HUNT_RETURN == m_eAutoHuntState)
{
__ClearAutoAttackTargetActorID();
__ClearReservedAction();
if (AUTO_HUNT_RETURN == m_eAutoHuntState)
if (CInstanceBase* pkInstMain = NEW_GetMainActorPtr())
pkInstMain->NEW_Stop();
}
m_eAutoHuntState = AUTO_HUNT_OFF;
m_eAutoHuntStopReason = eReason;
m_dwAutoHuntTargetVID = 0;
m_dwAutoHuntItemID = 0;
m_isAutoHuntGathering = false;
m_kSet_dwAutoHuntGathered.clear();
}
// Direction keys, the touch stick, a world click or the attack key: the player takes over, and the
// hunt picks up again from wherever they leave the character once they stop for a moment.
void CPythonPlayer::__AutoHunt_OnManualInput()
{
if (!AutoHunt_IsEnabled())
return;
if (AUTO_HUNT_PAUSED != m_eAutoHuntState)
{
__ClearAutoAttackTargetActorID();
__ClearReservedAction();
m_dwAutoHuntTargetVID = 0;
m_dwAutoHuntItemID = 0;
m_eAutoHuntState = AUTO_HUNT_PAUSED;
}
m_fAutoHuntResumeTime = CTimer::Instance().GetCurrentSecond() + c_fAutoHuntResumeDelay;
}
void CPythonPlayer::__AutoHunt_Engage(DWORD dwVID, float fNow)
{
m_dwAutoHuntTargetVID = dwVID;
m_eAutoHuntState = AUTO_HUNT_ENGAGE;
m_fAutoHuntStateTime = fNow;
m_fAutoHuntProgressTime = fNow;
m_fAutoHuntBestDistance = 1e9f;
m_fAutoHuntNextActionTime = fNow + 0.5f;
m_fAutoHuntHitTime = 0.0f;
SetTarget(dwVID);
__ClearReservedAction();
__SetAutoAttackTargetActorID(dwVID);
}
DWORD CPythonPlayer::__AutoHunt_FindTarget(CInstanceBase& rkInstMain, float fNow, bool isExceptGathered)
{
CPythonCharacterManager& rkChrMgr = CPythonCharacterManager::Instance();
CPythonNonPlayer& rkNonPlayer = CPythonNonPlayer::Instance();
DWORD dwBestVID = 0;
float fBestDistance = 1e9f;
for (CPythonCharacterManager::CharacterIterator i = rkChrMgr.CharacterInstanceBegin(); i != rkChrMgr.CharacterInstanceEnd(); ++i)
{
CInstanceBase* pkInst = *i;
if (!pkInst || pkInst == &rkInstMain || pkInst->IsDead())
continue;
if (!pkInst->IsEnemy() && !(m_kAutoHuntConfig.bAttackStone && pkInst->IsStone()))
continue;
if (!rkInstMain.IsAttackableInstance(*pkInst) || pkInst->IsInSafe())
continue;
if (!m_kAutoHuntConfig.bAttackBoss)
{
const CPythonNonPlayer::TMobTable* pMobTable = rkNonPlayer.GetTable(pkInst->GetRace());
if (pMobTable && pMobTable->bRank >= c_byMobRankBoss)
continue;
}
std::map<DWORD, float>::iterator f = m_kMap_dwAutoHuntSkipVID.find(pkInst->GetVirtualID());
if (f != m_kMap_dwAutoHuntSkipVID.end() && fNow < f->second)
continue;
if (isExceptGathered && m_kSet_dwAutoHuntGathered.count(pkInst->GetVirtualID()))
continue;
TPixelPosition kPPos;
pkInst->NEW_GetPixelPosition(&kPPos);
if (PixelDistance(kPPos, m_kPPosAutoHuntAnchor) > float(m_kAutoHuntConfig.iRange))
continue;
const float fDistance = rkInstMain.GetDistance(pkInst);
// The first pull may be anywhere in range; the rest near the pack that follows the character.
if (isExceptGathered && !m_kSet_dwAutoHuntGathered.empty() && fDistance > c_fAutoHuntGatherRange)
continue;
if (fDistance < fBestDistance)
{
fBestDistance = fDistance;
dwBestVID = pkInst->GetVirtualID();
}
}
return dwBestVID;
}
// Gather mode: the nearest pulled mob still alive.
DWORD CPythonPlayer::__AutoHunt_FindGatheredTarget(CInstanceBase& rkInstMain)
{
DWORD dwBestVID = 0;
float fBestDistance = 1e9f;
for (DWORD dwVID : m_kSet_dwAutoHuntGathered)
{
CInstanceBase* pkInst = NEW_FindActorPtr(dwVID);
if (!pkInst || pkInst->IsDead())
continue;
const float fDistance = rkInstMain.GetDistance(pkInst);
if (fDistance < fBestDistance)
{
fBestDistance = fDistance;
dwBestVID = dwVID;
}
}
return dwBestVID;
}
// Dead or out of sight.
void CPythonPlayer::__AutoHunt_PurgeGathered()
{
for (std::set<DWORD>::iterator i = m_kSet_dwAutoHuntGathered.begin(); i != m_kSet_dwAutoHuntGathered.end();)
{
CInstanceBase* pkInst = NEW_FindActorPtr(*i);
if (!pkInst || pkInst->IsDead())
i = m_kSet_dwAutoHuntGathered.erase(i);
else
++i;
}
}
bool CPythonPlayer::__AutoHunt_IsGatherDone(float fNow)
{
if (int(m_kSet_dwAutoHuntGathered.size()) >= m_kAutoHuntConfig.iGatherCount)
return true;
if (fNow - m_fAutoHuntGatherStartTime > c_fAutoHuntGatherTimeout)
return true;
const int iMaxHP = GetStatus(POINT_MAX_HP);
if (iMaxHP > 0 && GetStatus(POINT_HP) * 100 < iMaxHP * c_iAutoHuntGatherHPPercent)
{
AutoHuntTrace("gather stop: hp %d/%d", GetStatus(POINT_HP), iMaxHP);
return true;
}
return false;
}
DWORD CPythonPlayer::__AutoHunt_FindItem(CInstanceBase& rkInstMain, float fNow)
{
CPythonItem& rkItem = CPythonItem::Instance();
std::vector<DWORD> kVecVID;
rkItem.GetGroundItemVIDs(&kVecVID);
TPixelPosition kPPosMain;
rkInstMain.NEW_GetPixelPosition(&kPPosMain);
const float fRange = MIN(float(m_kAutoHuntConfig.iRange), c_fAutoHuntItemRange);
DWORD dwBestID = 0;
float fBestDistance = 1e9f;
for (DWORD dwID : kVecVID)
{
std::map<DWORD, float>::iterator f = m_kMap_dwAutoHuntSkipItem.find(dwID);
if (f != m_kMap_dwAutoHuntSkipItem.end() && fNow < f->second)
continue;
// Someone else's drop: the server refuses it until the ownership runs out.
const char* c_szOwner = NULL;
if (rkItem.GetOwnership(dwID, &c_szOwner) && c_szOwner && *c_szOwner && m_stName != c_szOwner)
continue;
if (AUTO_HUNT_PICKUP_GOLD_EQUIP == m_kAutoHuntConfig.iPickup)
{
const DWORD dwVnum = rkItem.GetVirtualNumberOfGroundItem(dwID);
CItemData* pItemData = NULL;
const bool isGold = c_dwGoldVnum == dwVnum;
const bool isEquip = CItemManager::Instance().GetItemDataPointer(dwVnum, &pItemData) && pItemData &&
(CItemData::ITEM_TYPE_WEAPON == pItemData->GetType() || CItemData::ITEM_TYPE_ARMOR == pItemData->GetType() ||
CItemData::ITEM_TYPE_ELK == pItemData->GetType());
if (!isGold && !isEquip)
continue;
}
TPixelPosition kPPosItem;
if (!rkItem.GetGroundItemPosition(dwID, &kPPosItem))
continue;
const float fDistance = PixelDistance(kPPosItem, kPPosMain);
if (fDistance > fRange)
continue;
if (fDistance < fBestDistance)
{
fBestDistance = fDistance;
dwBestID = dwID;
}
}
return dwBestID;
}
// uiinventory's cells: a big item covers the cells below its own, page by page.
bool CPythonPlayer::__AutoHunt_IsInventoryFull()
{
const DWORD c_dwColumns = 5;
bool abCovered[c_ItemSlot_Count] = {};
for (DWORD i = 0; i < c_ItemSlot_Count; ++i)
{
const DWORD dwVnum = GetItemIndex(TItemPos(INVENTORY, WORD(i)));
if (!dwVnum)
continue;
DWORD dwSize = 1;
CItemData* pItemData = NULL;
if (CItemManager::Instance().GetItemDataPointer(dwVnum, &pItemData) && pItemData)
dwSize = MAX(1, pItemData->GetSize());
const DWORD dwPageEnd = (i / c_Inventory_Page_Size + 1) * c_Inventory_Page_Size;
for (DWORD k = 0; k < dwSize && i + k * c_dwColumns < dwPageEnd; ++k)
abCovered[i + k * c_dwColumns] = true;
}
for (DWORD i = 0; i < c_ItemSlot_Count; ++i)
if (!abCovered[i])
return false;
return true;
}
// __CheckSkillUsable without its "cannot use" chat lines: the hunt only casts what will go through.
bool CPythonPlayer::__AutoHunt_IsSkillReady(DWORD dwSkillSlot, CPythonSkill::TSkillData** ppSkillData)
{
if (dwSkillSlot >= SKILL_MAX_NUM)
return false;
CInstanceBase* pkInstMain = NEW_GetMainActorPtr();
if (!pkInstMain)
return false;
TSkillInstance& rkSkillInst = m_playerStatus.aSkill[dwSkillSlot];
if (!rkSkillInst.dwIndex || rkSkillInst.iLevel <= 0)
return false;
CPythonSkill::TSkillData* pSkillData;
if (!CPythonSkill::Instance().GetSkillData(rkSkillInst.dwIndex, &pSkillData))
return false;
if (CPythonSkill::SKILL_TYPE_ACTIVE != pSkillData->byType && CPythonSkill::SKILL_TYPE_HORSE != pSkillData->byType)
return false;
if (!pSkillData->IsCanUseSkill())
return false;
if ((pkInstMain->IsMountingHorse() ? true : false) != (pSkillData->IsHorseSkill() ? true : false))
return false;
if (!pSkillData->CanUseWeaponType(pkInstMain->GetWeaponType()))
return false;
if (pSkillData->IsNeedBow() && !__HasEnoughArrow())
return false;
if (pSkillData->IsNeedEmptyBottle() && !__HasItem(27995))
return false;
if (pSkillData->IsNeedPoisonBottle() && !__HasItem(27996))
return false;
if (__CheckRestSkillCoolTime(dwSkillSlot))
return false;
if (__CheckShortLife(rkSkillInst, *pSkillData))
return false;
const int iNeedSP = pSkillData->GetNeedSP(rkSkillInst.fcurEfficientPercentage);
const int iCurSP = GetStatus(POINT_SP);
if (pSkillData->CanUseIfNotEnough() ? iCurSP <= 0 : iCurSP < iNeedSP)
return false;
*ppSkillData = pSkillData;
return true;
}
// Self buffs from the enabled quick slots whose effect is not on the character.
bool CPythonPlayer::__AutoHunt_UpdateBuff(CInstanceBase& rkInstMain, float fNow)
{
if (fNow < m_fAutoHuntNextSkillTime || rkInstMain.IsUsingSkill() || rkInstMain.IsInSafe())
return false;
for (DWORD i = 0; i < AUTO_HUNT_SKILL_SLOT_NUM; ++i)
{
if (!(m_kAutoHuntConfig.dwSkillSlotMask & (1 << i)))
continue;
DWORD dwType = SLOT_TYPE_NONE, dwSkillSlot = 0;
GetLocalQuickSlotData(i, &dwType, &dwSkillSlot);
if (SLOT_TYPE_SKILL != dwType)
continue;
CPythonSkill::TSkillData* pSkillData;
if (!__AutoHunt_IsSkillReady(dwSkillSlot, &pSkillData))
continue;
if (pSkillData->IsAttackSkill())
continue;
if (!pSkillData->IsStandingSkill() && !pSkillData->CanUseForMe())
continue;
if (pSkillData->IsToggleSkill())
{
if (IsSkillActive(dwSkillSlot))
continue;
}
else
{
bool isActive = false;
const DWORD dwSkillIndex = m_playerStatus.aSkill[dwSkillSlot].dwIndex;
for (std::map<DWORD, DWORD>::iterator f = m_kMap_dwAffectIndexToSkillIndex.begin(); f != m_kMap_dwAffectIndexToSkillIndex.end(); ++f)
if (f->second == dwSkillIndex && rkInstMain.IsAffect(f->first))
isActive = true;
if (isActive)
continue;
}
m_fAutoHuntNextSkillTime = fNow + 0.5f;
if (__UseSkill(dwSkillSlot))
return true;
}
return false;
}
void CPythonPlayer::__AutoHunt_UpdateAttackSkill(CInstanceBase& rkInstMain, CInstanceBase& rkInstVictim, float fNow)
{
if (fNow < m_fAutoHuntNextSkillTime || rkInstMain.IsUsingSkill())
return;
m_fAutoHuntNextSkillTime = fNow + 0.3f;
const float fDistance = rkInstMain.GetDistance(&rkInstVictim);
for (DWORD i = 0; i < AUTO_HUNT_SKILL_SLOT_NUM; ++i)
{
if (!(m_kAutoHuntConfig.dwSkillSlotMask & (1 << i)))
continue;
DWORD dwType = SLOT_TYPE_NONE, dwSkillSlot = 0;
GetLocalQuickSlotData(i, &dwType, &dwSkillSlot);
if (SLOT_TYPE_SKILL != dwType)
continue;
CPythonSkill::TSkillData* pSkillData;
if (!__AutoHunt_IsSkillReady(dwSkillSlot, &pSkillData) || !pSkillData->IsAttackSkill())
continue;
// Ranged skills fire from their range; the rest once the auto attack has walked up.
const float fSkillRange = MAX(float(pSkillData->GetTargetRange()), c_fAutoHuntSkillDistance);
if (fDistance > fSkillRange)
continue;
SetTarget(rkInstVictim.GetVirtualID());
if (__UseSkill(dwSkillSlot))
{
m_fAutoHuntNextSkillTime = fNow + 0.5f;
return;
}
}
}
// uitaskbar's potions: the first USE_POTION stack in the inventory that restores what is low.
void CPythonPlayer::__AutoHunt_UpdatePotion(CInstanceBase& rkInstMain, float fNow)
{
if (fNow < m_fAutoHuntNextPotionTime)
return;
m_fAutoHuntNextPotionTime = fNow + 0.5f;
for (int iValue = 0; iValue < 2; ++iValue)
{
const bool isHP = 0 == iValue;
const int iPercent = isHP ? m_kAutoHuntConfig.iHPPercent : m_kAutoHuntConfig.iSPPercent;
const int iMax = GetStatus(isHP ? POINT_MAX_HP : POINT_MAX_SP);
const int iCur = GetStatus(isHP ? POINT_HP : POINT_SP);
bool& isNoPotion = isHP ? m_isAutoHuntNoHPPotion : m_isAutoHuntNoSPPotion;
if (iPercent <= 0 || iMax <= 0 || iCur * 100 >= iMax * iPercent)
continue;
isNoPotion = true;
for (DWORD i = 0; i < c_ItemSlot_Count; ++i)
{
const TItemPos kCell(INVENTORY, WORD(i));
CItemData* pItemData = NULL;
if (!CItemManager::Instance().GetItemDataPointer(GetItemIndex(kCell), &pItemData) || !pItemData)
continue;
if (CItemData::ITEM_TYPE_USE != pItemData->GetType() || CItemData::USE_POTION != pItemData->GetSubType())
continue;
if (pItemData->GetValue(iValue) <= 0)
continue;
isNoPotion = false;
CPythonNetworkStream::Instance().SendItemUsePacket(kCell);
m_fAutoHuntNextPotionTime = fNow + 1.0f;
return;
}
}
}
void CPythonPlayer::__Update_AutoHunt()
{
static int s_iLastState = AUTO_HUNT_OFF;
if (s_iLastState != m_eAutoHuntState)
{
AutoHuntTrace("state %d -> %d (reason %d, target %u, item %u, gathering %d, gathered %d)", s_iLastState,
m_eAutoHuntState, m_eAutoHuntStopReason, (unsigned)m_dwAutoHuntTargetVID, (unsigned)m_dwAutoHuntItemID,
m_isAutoHuntGathering ? 1 : 0, int(m_kSet_dwAutoHuntGathered.size()));
s_iLastState = m_eAutoHuntState;
}
if (!AutoHunt_IsEnabled())
return;
CInstanceBase* pkInstMain = NEW_GetMainActorPtr();
if (!pkInstMain)
return;
CInstanceBase& rkInstMain = *pkInstMain;
if (rkInstMain.IsDead())
{
__AutoHunt_Stop(AUTO_HUNT_STOP_DEAD);
return;
}
if (m_stAutoHuntMapName != CPythonBackground::Instance().GetWarpMapName())
{
__AutoHunt_Stop(AUTO_HUNT_STOP_MAP);
return;
}
const float fNow = CTimer::Instance().GetCurrentSecond();
const float fElapsed = fMINMAX(0.0f, fNow - m_fAutoHuntLastUpdateTime, 0.1f);
m_fAutoHuntLastUpdateTime = fNow;
__AutoHunt_UpdatePotion(rkInstMain, fNow);
if (IsOpenPrivateShop() || IsObserverMode() || rkInstMain.IsFishingMode())
return;
switch (m_eAutoHuntState)
{
case AUTO_HUNT_PAUSED:
{
if (fNow < m_fAutoHuntResumeTime)
return;
rkInstMain.NEW_GetPixelPosition(&m_kPPosAutoHuntAnchor);
m_eAutoHuntState = AUTO_HUNT_SEARCH;
m_fAutoHuntStateTime = fNow;
break;
}
case AUTO_HUNT_SEARCH:
{
if (__AutoHunt_UpdateBuff(rkInstMain, fNow))
return;
if (fNow < m_fAutoHuntNextActionTime || rkInstMain.IsUsingSkill())
return;
m_fAutoHuntNextActionTime = fNow + 0.25f;
// Gather mode: hit up to N mobs once each so they follow, then fight them where they stand.
__AutoHunt_PurgeGathered();
if (m_kAutoHuntConfig.iGatherCount > 0 && !m_isAutoHuntGathering && m_kSet_dwAutoHuntGathered.empty())
{
m_isAutoHuntGathering = true;
m_fAutoHuntGatherStartTime = fNow;
}
if (m_isAutoHuntGathering)
{
DWORD dwVID = __AutoHunt_IsGatherDone(fNow) ? 0 : __AutoHunt_FindTarget(rkInstMain, fNow, true);
if (dwVID)
{
__AutoHunt_Engage(dwVID, fNow);
return;
}
m_isAutoHuntGathering = false;
if (!m_kSet_dwAutoHuntGathered.empty())
AutoHuntTrace("gather done: %d mobs, %.1fs", int(m_kSet_dwAutoHuntGathered.size()),
fNow - m_fAutoHuntGatherStartTime);
}
if (DWORD dwVID = __AutoHunt_FindGatheredTarget(rkInstMain))
{
__AutoHunt_Engage(dwVID, fNow);
return;
}
if (DWORD dwVID = __AutoHunt_FindTarget(rkInstMain, fNow))
{
__AutoHunt_Engage(dwVID, fNow);
return;
}
// Nothing to fight: drops the post-kill window missed (a kill farther than the loot range).
if (AUTO_HUNT_PICKUP_NONE != m_kAutoHuntConfig.iPickup && __AutoHunt_FindItem(rkInstMain, fNow))
{
m_dwAutoHuntItemID = 0;
m_eAutoHuntState = AUTO_HUNT_LOOT;
m_fAutoHuntStateTime = fNow - c_fAutoHuntLootDelay;
m_fAutoHuntNextActionTime = 0.0f;
return;
}
TPixelPosition kPPosMain;
rkInstMain.NEW_GetPixelPosition(&kPPosMain);
if (PixelDistance(kPPosMain, m_kPPosAutoHuntAnchor) > c_fAutoHuntReturnDistance)
{
m_eAutoHuntState = AUTO_HUNT_RETURN;
m_fAutoHuntStateTime = fNow;
m_fAutoHuntNextActionTime = 0.0f;
}
break;
}
case AUTO_HUNT_RETURN:
{
if (fNow < m_fAutoHuntNextActionTime)
return;
m_fAutoHuntNextActionTime = fNow + 0.5f;
if (DWORD dwVID = __AutoHunt_FindTarget(rkInstMain, fNow))
{
__AutoHunt_Engage(dwVID, fNow);
return;
}
TPixelPosition kPPosMain;
rkInstMain.NEW_GetPixelPosition(&kPPosMain);
if (PixelDistance(kPPosMain, m_kPPosAutoHuntAnchor) < c_fAutoHuntReturnDistance * 0.5f ||
fNow - m_fAutoHuntStateTime > c_fAutoHuntReturnTimeout)
{
rkInstMain.NEW_Stop();
m_eAutoHuntState = AUTO_HUNT_SEARCH;
m_fAutoHuntStateTime = fNow;
return;
}
if (!rkInstMain.IsWalking())
rkInstMain.NEW_MoveToDestPixelPositionDirection(m_kPPosAutoHuntAnchor);
break;
}
case AUTO_HUNT_ENGAGE:
{
CInstanceBase* pkInstVictim = NEW_FindActorPtr(m_dwAutoHuntTargetVID);
if (!pkInstVictim || pkInstVictim->IsDead())
{
__ClearAutoAttackTargetActorID();
m_dwAutoHuntTargetVID = 0;
m_dwAutoHuntItemID = 0;
// The rest of the pack is still on the character: loot once it is down.
__AutoHunt_PurgeGathered();
const bool isPackAlive = m_isAutoHuntGathering || !m_kSet_dwAutoHuntGathered.empty();
m_eAutoHuntState = AUTO_HUNT_PICKUP_NONE == m_kAutoHuntConfig.iPickup || isPackAlive ? AUTO_HUNT_SEARCH : AUTO_HUNT_LOOT;
m_fAutoHuntStateTime = fNow;
m_fAutoHuntNextActionTime = 0.0f;
return;
}
const float fDistance = rkInstMain.GetDistance(pkInstVictim);
if (fDistance < m_fAutoHuntBestDistance - 30.0f)
{
m_fAutoHuntBestDistance = fDistance;
m_fAutoHuntProgressTime = fNow;
}
if (rkInstMain.IsAttacking() || rkInstMain.IsUsingSkill())
{
m_fAutoHuntProgressTime = fNow;
m_fAutoHuntHitTime += fElapsed;
}
// Gather: this one is hit and follows now; go and hit the next.
if (m_isAutoHuntGathering && m_fAutoHuntHitTime >= c_fAutoHuntTagHitTime &&
!m_kSet_dwAutoHuntGathered.count(m_dwAutoHuntTargetVID))
{
m_kSet_dwAutoHuntGathered.insert(m_dwAutoHuntTargetVID);
__AutoHunt_PurgeGathered();
AutoHuntTrace("gather tag %u (%d/%d)", (unsigned)m_dwAutoHuntTargetVID, int(m_kSet_dwAutoHuntGathered.size()),
m_kAutoHuntConfig.iGatherCount);
}
if (m_isAutoHuntGathering && m_kSet_dwAutoHuntGathered.count(m_dwAutoHuntTargetVID) && !rkInstMain.IsUsingSkill())
{
DWORD dwNextVID = __AutoHunt_IsGatherDone(fNow) ? 0 : __AutoHunt_FindTarget(rkInstMain, fNow, true);
if (dwNextVID)
{
__AutoHunt_Engage(dwNextVID, fNow);
return;
}
m_isAutoHuntGathering = false;
AutoHuntTrace("gather done: %d mobs, %.1fs", int(m_kSet_dwAutoHuntGathered.size()),
fNow - m_fAutoHuntGatherStartTime);
}
// Blocked path, a target out of reach, or a safe zone that refuses the attack.
const bool isBlockedBySafeZone = rkInstMain.IsInSafe() && rkInstMain.NEW_IsClickableDistanceDestInstance(*pkInstVictim);
if (isBlockedBySafeZone || fNow - m_fAutoHuntProgressTime > c_fAutoHuntNoProgressTime ||
fNow - m_fAutoHuntStateTime > c_fAutoHuntTargetTimeout)
{
m_kMap_dwAutoHuntSkipVID[m_dwAutoHuntTargetVID] = fNow + c_fAutoHuntSkipTime;
__ClearAutoAttackTargetActorID();
__ClearReservedAction();
m_dwAutoHuntTargetVID = 0;
m_eAutoHuntState = AUTO_HUNT_SEARCH;
m_fAutoHuntStateTime = fNow;
return;
}
__AutoHunt_UpdateBuff(rkInstMain, fNow);
if (!m_isAutoHuntGathering)
__AutoHunt_UpdateAttackSkill(rkInstMain, *pkInstVictim, fNow);
// A cast or an interrupted approach drops the auto-attack target; put it back.
if (m_dwAutoAttackTargetVID != m_dwAutoHuntTargetVID && fNow >= m_fAutoHuntNextActionTime &&
!rkInstMain.IsUsingSkill() && MODE_USE_SKILL != m_eReservedMode)
{
m_fAutoHuntNextActionTime = fNow + 0.5f;
SetTarget(m_dwAutoHuntTargetVID);
__SetAutoAttackTargetActorID(m_dwAutoHuntTargetVID);
}
break;
}
case AUTO_HUNT_LOOT:
{
if (fNow - m_fAutoHuntStateTime < c_fAutoHuntLootDelay || fNow < m_fAutoHuntNextActionTime)
return;
m_fAutoHuntNextActionTime = fNow + 0.2f;
CPythonItem& rkItem = CPythonItem::Instance();
TPixelPosition kPPosItem;
if (m_dwAutoHuntItemID)
{
if (!rkItem.GetGroundItemPosition(m_dwAutoHuntItemID, &kPPosItem))
{
m_dwAutoHuntItemID = 0; // picked up (or gone)
}
else if (fNow - m_fAutoHuntProgressTime > c_fAutoHuntItemTimeout)
{
m_kMap_dwAutoHuntSkipItem[m_dwAutoHuntItemID] = fNow + c_fAutoHuntItemSkipTime;
m_dwAutoHuntItemID = 0;
__ClearReservedAction();
}
else
{
if (MODE_CLICK_ITEM != m_eReservedMode && !rkInstMain.IsWalking())
__ReserveClickItem(m_dwAutoHuntItemID);
return;
}
}
const DWORD dwItemID = fNow - m_fAutoHuntStateTime < c_fAutoHuntLootTimeout ? __AutoHunt_FindItem(rkInstMain, fNow) : 0;
if (!dwItemID || !rkItem.GetGroundItemPosition(dwItemID, &kPPosItem))
{
m_eAutoHuntState = AUTO_HUNT_SEARCH;
m_fAutoHuntStateTime = fNow;
m_fAutoHuntNextActionTime = 0.0f;
return;
}
if (c_dwGoldVnum != rkItem.GetVirtualNumberOfGroundItem(dwItemID) && __AutoHunt_IsInventoryFull())
{
__AutoHunt_Stop(AUTO_HUNT_STOP_INVENTORY_FULL);
return;
}
// __OnPressItem without its once-per-item guard.
m_dwAutoHuntItemID = dwItemID;
m_fAutoHuntProgressTime = fNow;
__ClearReservedAction();
if (rkInstMain.NEW_IsClickableDistanceDestPixelPosition(kPPosItem))
{
rkInstMain.NEW_Stop();
SendClickItemPacket(dwItemID);
}
else
{
__ReserveClickItem(dwItemID);
}
break;
}
}
}
@@ -442,8 +442,11 @@ void CPythonPlayer::__OnPressScreen(CInstanceBase& rkInstMain)
}
bool CPythonPlayer::NEW_MoveToDirection(float fDirRot)
bool CPythonPlayer::NEW_MoveToDirection(float fDirRot, bool isAutoCameraRotation)
{
// PORT: direction keys and the touch stick pause auto hunt.
__AutoHunt_OnManualInput();
// PrivateShop
if (IsOpenPrivateShop())
return true;
@@ -464,7 +467,7 @@ bool CPythonPlayer::NEW_MoveToDirection(float fDirRot)
float fCmrCurRot=CameraRotationToCharacterRotation(pkCmrCur->GetRoll());
// 현재
if (m_isCmrRot)
if (m_isCmrRot && isAutoCameraRotation)
{
float fSigDirRot=fDirRot;
if (fSigDirRot>180.0f)
@@ -7,6 +7,7 @@ void CPythonPlayer::SetAttackKeyState(bool isPress)
{
if (isPress)
{
__AutoHunt_OnManualInput(); // PORT: auto hunt
CInstanceBase* pkInstMain = NEW_GetMainActorPtr();
if (pkInstMain)
if (pkInstMain->IsFishingMode())
@@ -97,6 +97,10 @@ void CPythonPlayer::NEW_SetMouseSmartState(int eMBS, bool isAuto)
CInstanceBase* pkInstMain=NEW_GetMainActorPtr();
if (!pkInstMain) return;
// PORT: a click in the world hands control back to the player for a moment (auto hunt).
if (MBS_PRESS == eMBS)
__AutoHunt_OnManualInput();
// PrivateShop
if (IsOpenPrivateShop())
{
@@ -2166,6 +2166,81 @@ PyObject* playerSendDragonSoulRefine(PyObject* poSelf, PyObject* poArgs)
return Py_BuildNone();
}
// PORT: auto hunt (PythonPlayerAutoHunt.cpp); 40250 has no counterpart.
PyObject * playerAutoHuntSetEnabled(PyObject* poSelf, PyObject* poArgs)
{
int isEnable;
if (!PyTuple_GetInteger(poArgs, 0, &isEnable))
return Py_BuildException();
CPythonPlayer::Instance().AutoHunt_SetEnabled(isEnable ? true : false);
return Py_BuildNone();
}
PyObject * playerAutoHuntIsEnabled(PyObject* poSelf, PyObject* poArgs)
{
return Py_BuildValue("i", CPythonPlayer::Instance().AutoHunt_IsEnabled());
}
// (state, stop reason, out of HP potions, out of SP potions, gathering, gathered mobs alive)
PyObject * playerAutoHuntGetState(PyObject* poSelf, PyObject* poArgs)
{
CPythonPlayer& rkPlayer = CPythonPlayer::Instance();
return Py_BuildValue("iiiiii", rkPlayer.AutoHunt_GetState(), rkPlayer.AutoHunt_GetStopReason(),
rkPlayer.AutoHunt_HasNoPotion(true), rkPlayer.AutoHunt_HasNoPotion(false),
rkPlayer.AutoHunt_IsGathering(), rkPlayer.AutoHunt_GetGatheredCount());
}
// (range, hp %, sp %, pickup, stone, boss, quick-slot skill mask, gather count)
PyObject * playerAutoHuntGetConfig(PyObject* poSelf, PyObject* poArgs)
{
const CPythonPlayer::SAutoHuntConfig& c_rkConfig = CPythonPlayer::Instance().AutoHunt_GetConfig();
return Py_BuildValue("iiiiiiii", c_rkConfig.iRange, c_rkConfig.iHPPercent, c_rkConfig.iSPPercent, c_rkConfig.iPickup,
c_rkConfig.bAttackStone, c_rkConfig.bAttackBoss, int(c_rkConfig.dwSkillSlotMask), c_rkConfig.iGatherCount);
}
PyObject * playerAutoHuntSetConfig(PyObject* poSelf, PyObject* poArgs)
{
int aiValue[8];
for (int i = 0; i < 8; ++i)
if (!PyTuple_GetInteger(poArgs, i, &aiValue[i]))
return Py_BuildException();
CPythonPlayer::SAutoHuntConfig kConfig;
kConfig.iRange = aiValue[0];
kConfig.iHPPercent = aiValue[1];
kConfig.iSPPercent = aiValue[2];
kConfig.iPickup = aiValue[3];
kConfig.bAttackStone = aiValue[4] != 0;
kConfig.bAttackBoss = aiValue[5] != 0;
kConfig.dwSkillSlotMask = DWORD(aiValue[6]);
kConfig.iGatherCount = aiValue[7];
CPythonPlayer::Instance().AutoHunt_SetConfig(kConfig);
return Py_BuildNone();
}
// The auto hunt window's UTF-8 source strings in the locale's code page (locale.cfg), as the packed
// locale_game.txt strings are.
PyObject * playerAutoHuntLocalize(PyObject* poSelf, PyObject* poArgs)
{
char* szText;
if (!PyTuple_GetString(poArgs, 0, &szText))
return Py_BuildException();
const int iWide = MultiByteToWideChar(CP_UTF8, 0, szText, -1, NULL, 0);
if (iWide <= 0)
return Py_BuildValue("s", szText);
std::vector<WCHAR> kVecWide(iWide);
MultiByteToWideChar(CP_UTF8, 0, szText, -1, &kVecWide[0], iWide);
const int iBytes = WideCharToMultiByte(GetDefaultCodePage(), 0, &kVecWide[0], -1, NULL, 0, NULL, NULL);
if (iBytes <= 0)
return Py_BuildValue("s", szText);
std::vector<char> kVecText(iBytes);
WideCharToMultiByte(GetDefaultCodePage(), 0, &kVecWide[0], -1, &kVecText[0], iBytes, NULL, NULL);
return Py_BuildValue("s", &kVecText[0]);
}
void initPlayer()
{
static PyMethodDef s_methods[] =
@@ -2337,6 +2412,13 @@ void initPlayer()
{ "SlotTypeToInvenType", playerSlotTypeToInvenType, METH_VARARGS },
{ "SendDragonSoulRefine", playerSendDragonSoulRefine, METH_VARARGS },
{ "AutoHuntSetEnabled", playerAutoHuntSetEnabled, METH_VARARGS },
{ "AutoHuntIsEnabled", playerAutoHuntIsEnabled, METH_VARARGS },
{ "AutoHuntGetState", playerAutoHuntGetState, METH_VARARGS },
{ "AutoHuntGetConfig", playerAutoHuntGetConfig, METH_VARARGS },
{ "AutoHuntSetConfig", playerAutoHuntSetConfig, METH_VARARGS },
{ "AutoHuntLocalize", playerAutoHuntLocalize, METH_VARARGS },
{ NULL, NULL, NULL },
};
@@ -1,6 +1,7 @@
#include "StdAfx.h"
#include "PythonTextTail.h"
#include "PythonCharacterManager.h"
#include "../EterPythonLib/PythonWindowManager.h"
PyObject * textTailClear(PyObject * poSelf, PyObject * poArgs)
{
@@ -173,6 +174,8 @@ PyObject * textTailPick(PyObject * poSelf, PyObject * poArgs)
if (!PyTuple_GetInteger(poArgs, 1, &iy))
return Py_BuildException();
// PORT: game.py passes window coordinates; the text tails sit at screen coordinates.
ix += UI::CWindowManager::Instance().GetScreenOffsetX();
int iValue = CPythonTextTail::Instance().Pick(ix, iy);
return Py_BuildValue("i", iValue);
}
+120 -5
View File
@@ -19,6 +19,9 @@
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_vulkan.h>
#ifdef __ANDROID__
#include <jni.h>
#endif
#include <vulkan/vulkan.h>
#include "stb_image.h"
@@ -714,6 +717,21 @@ std::string bundled_file_path(const char* name) {
return std::string(base ? base : "./") + name;
}
#ifdef __ANDROID__
// MainActivity.performHaptic(): the long-press vibration of the touch gestures.
void android_haptic() {
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
auto activity = static_cast<jobject>(SDL_GetAndroidActivity());
if (!env || !activity) return;
jclass cls = env->GetObjectClass(activity);
if (jmethodID method = env->GetMethodID(cls, "performHaptic", "()V"))
env->CallVoidMethod(activity, method);
if (env->ExceptionCheck()) env->ExceptionClear();
env->DeleteLocalRef(cls);
env->DeleteLocalRef(activity);
}
#endif
std::vector<std::uint32_t> read_spirv(const char* name) {
std::size_t size = 0;
void* bytes = SDL_LoadFile(bundled_file_path(name).c_str(), &size);
@@ -848,6 +866,8 @@ public:
}
}
#endif
// Android's back key reaches the game as SDL_SCANCODE_AC_BACK (-> ESC) instead of finishing the activity.
SDL_SetHint(SDL_HINT_ANDROID_TRAP_BACK_BUTTON, "1");
if (!SDL_Init(SDL_INIT_VIDEO)) throw std::runtime_error(SDL_GetError());
SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight");
window_ = SDL_CreateWindow(
@@ -1010,12 +1030,24 @@ public:
return {0, 0, float(extent_.width), float(extent_.height), 0, 1};
}
// Window pixels kept clear at the left and right screen edges (rounded corners, cutouts);
// MainActivity measures them and passes --safe-inset-px.
void set_safe_inset_px(int px) { safe_inset_px_ = std::max(0, px); }
// Test builds: frames per second in the top-left corner (--show-fps).
void set_show_fps(bool show) { show_fps_ = show; }
// The same inset in logical UI pixels: the 40250 window layers are shifted and narrowed by it.
int ui_safe_inset() const {
int pw = 0, ph = 0;
if (safe_inset_px_ <= 0 || !window_ || !SDL_GetWindowSizeInPixels(window_, &pw, &ph) || ph <= 0) return 0;
return int(std::lround(double(safe_inset_px_) * logical_height() / ph));
}
// D3DVIEWPORT8 (in the game's logical screen pixels) scaled to the swapchain extent.
VkViewport draw_viewport(const Render3DDraw& draw) const {
if (draw.viewport[2] <= 0 || draw.viewport[3] <= 0) return full_viewport();
const float sx = float(extent_.width) / float(logical_width());
const float sy = float(extent_.height) / float(logical_height());
return {draw.viewport[0] * sx, draw.viewport[1] * sy, draw.viewport[2] * sx, draw.viewport[3] * sy,
return {(draw.viewport[0] + draw.ui_offset_x) * sx, draw.viewport[1] * sy, draw.viewport[2] * sx, draw.viewport[3] * sy,
draw.viewport_z[0], draw.viewport_z[1]};
}
@@ -1160,6 +1192,24 @@ public:
}
}
// Touch hosts: SDL text input (and with it the system keyboard) is on only while the player
// types into a tapped EditLine; desktop keeps the always-on text input from window creation.
void sync_screen_keyboard() {
const bool want = touch_controller.wants_screen_keyboard();
const unsigned serial = touch_controller.screen_keyboard_serial();
if (want && (!SDL_TextInputActive(window_) || serial != screen_keyboard_serial_)) {
SDL_PropertiesID props = SDL_CreateProperties();
SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_TYPE_NUMBER, SDL_TEXTINPUT_TYPE_TEXT);
SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER, SDL_CAPITALIZE_NONE);
SDL_SetBooleanProperty(props, SDL_PROP_TEXTINPUT_AUTOCORRECT_BOOLEAN, false);
SDL_StartTextInputWithProperties(window_, props);
SDL_DestroyProperties(props);
} else if (!want && SDL_TextInputActive(window_)) {
SDL_StopTextInput(window_);
}
screen_keyboard_serial_ = serial;
}
bool poll(bool forward_to_live_client = false) {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
auto map_mouse = [&](float wx, float wy, int& ox, int& oy) {
@@ -1190,6 +1240,13 @@ public:
SDL_Event event;
while (SDL_PollEvent(&event)) {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
// Touches arrive as SDL_EVENT_FINGER_*; the touch-synthesized mouse copy
// would register as a second finger (101) and turn every drag into a pinch.
if (touch_controller.is_enabled() &&
((event.type == SDL_EVENT_MOUSE_MOTION && event.motion.which == SDL_TOUCH_MOUSEID) ||
((event.type == SDL_EVENT_MOUSE_BUTTON_DOWN || event.type == SDL_EVENT_MOUSE_BUTTON_UP) &&
event.button.which == SDL_TOUCH_MOUSEID)))
continue;
if (forward_to_live_client && event.type == SDL_EVENT_MOUSE_MOTION) {
int mx = 0, my = 0;
map_mouse(event.motion.x, event.motion.y, mx, my);
@@ -1204,9 +1261,10 @@ public:
if (event.type == SDL_EVENT_WINDOW_RESIZED || event.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED) {
int ew = 0, eh = 0;
SDL_GetWindowSize(window_, &ew, &eh);
touch_controller.update_screen_size(int(logical_width()), int(logical_height()));
touch_controller.update_screen_size(int(logical_width()), int(logical_height()), ui_safe_inset());
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
if (forward_to_live_client && ew > 0 && eh > 0) {
PythonBoot::SetUISafeInset(ui_safe_inset());
PythonBoot::SetUISize(int(logical_width()), int(logical_height()));
}
#endif
@@ -1259,6 +1317,8 @@ public:
PythonBoot::UIMouseWheel(int(event.wheel.y * 120.0f));
} else if (event.type == SDL_EVENT_KEY_DOWN || event.type == SDL_EVENT_KEY_UP) {
const bool pressed = event.type == SDL_EVENT_KEY_DOWN;
// Android back = ESC: closes the top window, or opens the system menu (game.py).
if (event.key.scancode == SDL_SCANCODE_AC_BACK) event.key.scancode = SDL_SCANCODE_ESCAPE;
if (pressed) {
if (const int vk = sdl_scancode_to_vk(event.key.scancode))
PythonBoot::UIIMEKeyDown(vk);
@@ -1304,6 +1364,8 @@ public:
flush_mouse_move();
#endif
touch_controller.update();
if (touch_controller.is_enabled())
sync_screen_keyboard();
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
if (forward_to_live_client && !hardware_cursor_enabled_ && !os_cursor_hidden_ && !touch_controller.is_enabled()) {
SDL_HideCursor();
@@ -1452,6 +1514,8 @@ public:
prepared_draw.fixed_state = make_fixed_function_state(draw);
// XYZRHW vertices are already in screen space and bypass the viewport transform.
prepared_draw.viewport = draw.pretransformed ? full_viewport() : draw_viewport(draw);
if (draw.pretransformed && draw.ui_offset_x != 0)
prepared_draw.viewport.x += draw.ui_offset_x * float(extent_.width) / float(logical_width());
prepared_draws.push_back(prepared_draw);
vertex_count += geometry.vertex_count;
index_count += geometry.index_count;
@@ -1470,10 +1534,19 @@ public:
if (ui_mapped_) {
const uint32_t target_ui_w = ui_width ? ui_width : 960;
const uint32_t target_ui_h = ui_height ? ui_height : 640;
if (touch_controller.is_enabled()) {
touch_controller.update_screen_size(int(target_ui_w), int(target_ui_h));
update_fps_counter();
if (touch_controller.is_enabled() || show_fps_) {
std::vector<UIRenderCommand> combined_ui = ui_commands;
touch_controller.append_ui_commands(combined_ui);
if (touch_controller.is_enabled()) {
touch_controller.update_screen_size(int(target_ui_w), int(target_ui_h), ui_safe_inset());
touch_controller.append_ui_commands(combined_ui);
}
if (show_fps_ && fps_ >= 0) {
// Top left, inside the safe area and clear of 40250's corner icon.
const float inset = float(std::min<int>(ui_safe_inset(), int(target_ui_w) / 8));
TouchController::draw_segment_text(combined_ui, "FPS " + std::to_string(fps_),
inset + 40.0f, 8.0f, 12.0f, 0xFF40FF40);
}
if (!combined_ui.empty()) {
build_ui_batches(target_ui_w, target_ui_h, combined_ui, capture_textures, ui_batches, ui_quad_count);
}
@@ -3055,6 +3128,27 @@ private:
float timestamp_period_ns_ = 1.0f;
float max_anisotropy_ = 1.0f;
SDL_Window* window_ = nullptr;
unsigned screen_keyboard_serial_ = 0;
int safe_inset_px_ = 0;
bool show_fps_ = false;
int fps_ = -1;
int fps_frames_ = 0;
std::chrono::steady_clock::time_point fps_window_start_{};
// Presented frames over the last half second.
void update_fps_counter() {
const auto now = std::chrono::steady_clock::now();
if (fps_frames_++ == 0) {
fps_window_start_ = now;
return;
}
const double elapsed = std::chrono::duration<double>(now - fps_window_start_).count();
if (elapsed >= 0.5) {
fps_ = int(std::lround((fps_frames_ - 1) / elapsed));
fps_frames_ = 1;
fps_window_start_ = now;
}
}
VkInstance instance_ = VK_NULL_HANDLE;
VkSurfaceKHR surface_ = VK_NULL_HANDLE;
VkPhysicalDevice physical_ = VK_NULL_HANDLE;
@@ -3198,6 +3292,10 @@ void print_summary(const VulkanWindow& renderer, int completed, double wall_ms,
}
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
// --py-exec: a test hook run once the auto-login run reaches the GameWindow (or, with --login-screen,
// once the LoginWindow is up).
std::string g_py_exec_after_login;
bool py_exec(const std::string& code) {
std::string err;
if (!PythonBoot::RunLine(code.c_str(), &err)) {
@@ -3342,6 +3440,7 @@ int run_live_client(
#endif
SetPlatformServerTime(123456789);
PythonBoot::SetUISafeInset(renderer.ui_safe_inset());
PythonBoot::SetUISize(static_cast<int>(renderer.logical_width()), static_cast<int>(renderer.logical_height()));
if (!PythonBoot::RunMainScript("", &error) || !PythonBoot::IsAppLooping())
throw std::runtime_error("PythonBoot::RunMainScript: " + error);
@@ -3396,6 +3495,8 @@ int run_live_client(
renderer.sync_game_cursor();
PythonBoot::UIRender();
}
if (!g_py_exec_after_login.empty() && !py_exec(g_py_exec_after_login))
throw std::runtime_error("--py-exec failed");
} else {
char login_cmd[512];
std::snprintf(
@@ -3455,6 +3556,8 @@ int run_live_client(
if (!configure_macos_numeric_quickslots())
throw std::runtime_error("failed to configure numeric quick slots on macOS");
#endif
if (!g_py_exec_after_login.empty() && !py_exec(g_py_exec_after_login))
throw std::runtime_error("--py-exec failed");
const int warmup_frames = std::max(10, fake_mobs / 2 + 10);
for (int i = 0; i < warmup_frames; ++i) {
@@ -3583,6 +3686,8 @@ int main(int argc, char** argv) {
bool gpu_skinning = true;
bool native_terrain = true;
bool mobile_mode = false;
int safe_inset_px = 0;
bool show_fps = false;
#ifdef __ANDROID__
mobile_mode = true;
#endif
@@ -3633,6 +3738,11 @@ int main(int argc, char** argv) {
else if (arg == "--no-gpu-skinning") gpu_skinning = false;
else if (arg == "--no-terrain") native_terrain = false;
else if (arg == "--mobile") mobile_mode = true;
else if (arg == "--safe-inset-px" && i+1 < argc) safe_inset_px = std::stoi(argv[++i]);
else if (arg == "--show-fps") show_fps = true;
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
else if (arg == "--py-exec" && i+1 < argc) g_py_exec_after_login = argv[++i];
#endif
else throw std::runtime_error(
"usage: mt_native_render [--frames N] [--interactive] [--login-screen|--selection-screen] "
"[--live-server HOST:AUTH_PORT:GAME_PORT] [--width W] [--height H] "
@@ -3677,9 +3787,14 @@ int main(int argc, char** argv) {
triangles_per_draw == 0 || triangles_per_draw > 10000)
throw std::runtime_error("invalid frame/draw/triangle count");
VulkanWindow renderer(vsync, init_width, init_height);
renderer.set_safe_inset_px(safe_inset_px);
renderer.set_show_fps(show_fps);
if (!screenshot_out.empty() && frames > 0) renderer.request_screenshot(screenshot_out, std::uint64_t(frames));
if (mobile_mode) {
renderer.touch_controller.set_enabled(true);
#ifdef __ANDROID__
renderer.touch_controller.set_haptic(android_haptic);
#endif
renderer.touch_controller.update_screen_size(init_width, init_height);
}
if (!live_client_dir.empty()) {
+323 -84
View File
@@ -17,7 +17,7 @@
class TouchController {
public:
struct ButtonDef {
int id; // 1 = attack, 2..7 = quick slots 1..6
int id; // 1 = attack, 2..7 = quick slots 1..6, 8 = auto hunt
int dik;
float x, y, radius;
const char* label;
@@ -31,15 +31,30 @@ public:
init_buttons();
}
void set_enabled(bool enabled) { enabled_ = enabled; }
void set_enabled(bool enabled) {
enabled_ = enabled;
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::SetTouchInput(enabled);
#endif
}
bool is_enabled() const { return enabled_; }
// Short vibration when a long press fires (the host wires it to the platform).
void set_haptic(void (*haptic)()) { haptic_ = haptic; }
void update_screen_size(int width, int height) {
// The system keyboard is up only after the player taps the focused EditLine, not when a
// script focuses one on its own (intrologin.py focuses the ID field as the board opens).
// The serial changes on every such tap so the host can re-show a keyboard the user dismissed.
bool wants_screen_keyboard() const { return keyboard_requested_; }
unsigned screen_keyboard_serial() const { return keyboard_serial_; }
// safe_inset: logical pixels kept clear at the left and right edges (rounded corners, cutouts).
void update_screen_size(int width, int height, int safe_inset = 0) {
if (width <= 0 || height <= 0) return;
screen_w_ = width;
screen_h_ = height;
safe_inset_ = float(std::max(0, safe_inset));
// Position joystick on lower-left
joystick_base_x_ = std::max(90.0f, float(width) * 0.12f);
joystick_base_x_ = safe_inset_ + std::max(90.0f, float(width) * 0.12f);
joystick_base_y_ = float(height) - std::max(90.0f, float(height) * 0.22f);
if (!joystick_active_) {
joystick_knob_x_ = joystick_base_x_;
@@ -51,6 +66,37 @@ public:
// Called once per frame to maintain continuous analog movement
void update() {
if (!enabled_) return;
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
if (keyboard_requested_ && !PythonBoot::TextInputFocused())
keyboard_requested_ = false;
// A world tap presses and releases on later frames than its mouse move so
// 40250's per-frame actor picking has already hit the target under the finger.
if (tap_stage_ == 1) {
PythonBoot::UIMouseMove(int(tap_x_), int(tap_y_));
PythonBoot::UIMouseButton(1, true, int(tap_x_), int(tap_y_));
tap_stage_ = 2;
} else if (tap_stage_ == 2) {
PythonBoot::UIMouseButton(1, false, int(tap_x_), int(tap_y_));
tap_stage_ = 0;
}
// A UI finger held still turns into a right click (use item, equip, skill up...).
if (ui_touch_finger_id_ >= 0 && ui_gesture_ == UiGesture::Pending &&
get_time_sec() - ui_down_time_ >= kLongPressSec) {
ui_gesture_ = UiGesture::LongPressed;
last_tap_time_ = -1.0;
PythonBoot::UIMouseButton(2, true, int(ui_start_x_), int(ui_start_y_));
PythonBoot::UIMouseButton(2, false, int(ui_start_x_), int(ui_start_y_));
if (haptic_) haptic_();
}
for (auto& btn : buttons_) {
if (btn.id == kAutoHuntButton && btn.pressed && !auto_hunt_long_pressed_ &&
get_time_sec() - auto_hunt_down_time_ >= kLongPressSec) {
auto_hunt_long_pressed_ = true;
PythonBoot::AutoHuntOpenSettings();
if (haptic_) haptic_();
}
}
#endif
const bool controls_visible = gameplay_controls_visible();
if (!controls_visible) {
if (controls_were_visible_)
@@ -79,11 +125,17 @@ public:
if (btn_idx >= 0) {
auto& btn = buttons_[btn_idx];
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
if (btn.id >= 2 && PythonBoot::TryAssignAttachedObjectToLocalQuickSlot(btn.id - 2))
if (is_quick_slot_button(btn.id) && PythonBoot::TryAssignAttachedObjectToLocalQuickSlot(btn.id - 2))
return true;
#endif
btn.pressed = true;
btn.finger_id = finger_id;
if (btn.id == kAutoHuntButton) {
// Tap toggles; holding opens the settings window (update()).
auto_hunt_down_time_ = get_time_sec();
auto_hunt_long_pressed_ = false;
return true;
}
trigger_button(btn.dik, true);
return true;
}
@@ -91,16 +143,21 @@ public:
// Everything else uses the unchanged 40250 UI and its normal mouse path.
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
// Only hover here (the tooltip shows while the finger rests); what the finger does next picks
// the gesture: lift = click, slide = press + drag, hold = right click, second tap = double click.
if (PythonBoot::IsPointInsideActiveUI(int(px), int(py))) {
ui_touch_finger_id_ = finger_id;
ui_gesture_ = UiGesture::Pending;
ui_start_x_ = px;
ui_start_y_ = py;
ui_down_time_ = get_time_sec();
PythonBoot::UIMouseMove(int(px), int(py));
PythonBoot::UIMouseButton(1, true, int(px), int(py));
return true;
}
#endif
// Start movement only in the gameplay phase.
if (controls_visible && norm_x < 0.40f && norm_y > 0.35f && !joystick_active_) {
// Mobile-game layout: the left half moves (floating stick), the right half looks.
if (controls_visible && norm_x < kMoveZoneRight && norm_y > kMoveZoneTop && !joystick_active_) {
joystick_active_ = true;
joystick_finger_id_ = finger_id;
joystick_base_x_ = px;
@@ -114,20 +171,28 @@ public:
if (!controls_visible)
return false;
// The remaining gameplay area controls camera and world selection.
if (camera_finger_id_ < 0) {
camera_finger_id_ = finger_id;
camera_last_x_ = px;
camera_last_y_ = py;
camera_start_x_ = px;
camera_start_y_ = py;
camera_dragged_ = false;
camera_down_time_ = get_time_sec();
const bool in_look_zone = norm_x >= kMoveZoneRight;
if (look_finger_id_ < 0) {
// Outside the look zone the finger can only tap-select.
look_finger_id_ = finger_id;
look_can_rotate_ = in_look_zone;
look_rotating_ = false;
look_moved_ = false;
look_start_x_ = look_last_x_ = px;
look_start_y_ = look_last_y_ = py;
look_down_time_ = get_time_sec();
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::UIMouseMove(int(px), int(py)); // start picking under the finger
#endif
return true;
} else if (pinch_finger2_ < 0) {
pinch_finger1_ = camera_finger_id_;
pinch_finger2_ = finger_id;
pinch_last_dist_ = std::hypot(px - camera_last_x_, py - camera_last_y_);
}
if (pinch_finger_id_ < 0 && look_can_rotate_ && in_look_zone) {
pinch_finger_id_ = finger_id;
pinch_x_ = px;
pinch_y_ = py;
pinch_last_dist_ = std::hypot(px - look_last_x_, py - look_last_y_);
look_rotating_ = false;
look_moved_ = true; // a pinch never ends as a tap
return true;
}
@@ -143,6 +208,14 @@ public:
// 1. UI Touch dragging (e.g. dragging item in inventory or scrollbar)
if (ui_touch_finger_id_ == finger_id) {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
if (ui_gesture_ == UiGesture::Pending &&
std::hypot(px - ui_start_x_, py - ui_start_y_) > kUiDragSlop) {
// 40250 drag: press where the finger landed (a slot attaches its icon, a title bar
// or scroll bar captures), then follow the finger.
ui_gesture_ = UiGesture::Dragging;
last_tap_time_ = -1.0;
PythonBoot::UIMouseButton(1, true, int(ui_start_x_), int(ui_start_y_));
}
PythonBoot::UIMouseMove(int(px), int(py));
#endif
return true;
@@ -169,38 +242,42 @@ public:
}
}
// 4. Two-finger pinch zoom
if (pinch_finger1_ >= 0 && pinch_finger2_ >= 0 &&
(finger_id == pinch_finger1_ || finger_id == pinch_finger2_)) {
const float cur_dist = std::hypot(px - camera_last_x_, py - camera_last_y_);
if (pinch_last_dist_ > 1.0f && cur_dist > 1.0f) {
const float delta_d = cur_dist - pinch_last_dist_;
if (std::abs(delta_d) > 2.0f) {
// 4. Two-finger pinch zoom (look finger + second right-half finger)
if (pinch_finger_id_ >= 0 && (finger_id == pinch_finger_id_ || finger_id == look_finger_id_)) {
if (finger_id == pinch_finger_id_) {
pinch_x_ = px;
pinch_y_ = py;
} else {
look_last_x_ = px;
look_last_y_ = py;
}
const float cur_dist = std::hypot(pinch_x_ - look_last_x_, pinch_y_ - look_last_y_);
const float delta_d = cur_dist - pinch_last_dist_;
if (std::abs(delta_d) > 2.0f) {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::UIMouseWheel(int(delta_d * 8.0f));
PythonBoot::UIMouseWheel(int(delta_d * 8.0f));
#endif
pinch_last_dist_ = cur_dist;
}
pinch_last_dist_ = cur_dist;
}
return true;
}
// 5. Single-finger camera drag
if (finger_id == camera_finger_id_) {
const float total_dist = std::hypot(px - camera_start_x_, py - camera_start_y_);
if (total_dist > 6.0f) {
if (!camera_dragged_) {
camera_dragged_ = true;
// 5. Single-finger look drag: yaw by dx, pitch by dy, stops the moment the finger stops.
if (finger_id == look_finger_id_) {
if (!look_moved_ && std::hypot(px - look_start_x_, py - look_start_y_) > kLookDeadZone) {
look_moved_ = true;
look_rotating_ = look_can_rotate_;
look_last_x_ = px; // start from here so the dead zone does not jump the camera
look_last_y_ = py;
}
if (look_rotating_) {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::CameraBeginDrag(int(camera_start_x_), int(camera_start_y_));
#endif
}
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::CameraDrag(int(px), int(py));
PythonBoot::CameraRotateBy((px - look_last_x_) * kLookYawDegPerPx,
(py - look_last_y_) * kLookPitchDegPerPx);
#endif
}
camera_last_x_ = px;
camera_last_y_ = py;
look_last_x_ = px;
look_last_y_ = py;
return true;
}
@@ -217,7 +294,41 @@ public:
if (ui_touch_finger_id_ == finger_id) {
ui_touch_finger_id_ = -1;
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::UIMouseButton(1, false, int(px), int(py));
if (ui_gesture_ == UiGesture::Dragging) {
// Dropped on a skill button: that quick slot takes the dragged icon.
const int btn_idx = gameplay_controls_visible() ? find_button(px, py) : -1;
if (btn_idx >= 0 && is_quick_slot_button(buttons_[btn_idx].id) && PythonBoot::UIIsAttaching()) {
PythonBoot::TryAssignAttachedObjectToLocalQuickSlot(buttons_[btn_idx].id - 2);
PythonBoot::UIMouseButton(1, false, int(ui_start_x_), int(ui_start_y_));
} else {
PythonBoot::UIMouseButton(1, false, int(px), int(py));
}
} else if (ui_gesture_ == UiGesture::Pending) {
const double now = get_time_sec();
const int x = int(ui_start_x_), y = int(ui_start_y_);
if (last_tap_time_ >= 0.0 && now - last_tap_time_ < kDoubleTapSec &&
std::hypot(ui_start_x_ - last_tap_x_, ui_start_y_ - last_tap_y_) < kDoubleTapSlop) {
// Win32 sends DOWN, UP, DBLCLK, UP; the first click picked the icon up, which a
// mouse user would have seen and a finger hides, so put it back first.
if (last_tap_attached_ && PythonBoot::UIIsAttaching())
PythonBoot::UIDeattachObject();
PythonBoot::UIMouseDoubleClick(x, y);
PythonBoot::UIMouseButton(1, false, x, y);
last_tap_time_ = -1.0;
} else {
const bool was_attaching = PythonBoot::UIIsAttaching();
PythonBoot::UIMouseButton(1, true, x, y);
PythonBoot::UIMouseButton(1, false, x, y);
last_tap_time_ = now;
last_tap_x_ = ui_start_x_;
last_tap_y_ = ui_start_y_;
last_tap_attached_ = !was_attaching && PythonBoot::UIIsAttaching();
}
keyboard_requested_ = PythonBoot::TextInputFocused() &&
PythonBoot::IsPointInsideFocusedWindow(x, y);
if (keyboard_requested_) ++keyboard_serial_;
}
ui_gesture_ = UiGesture::None;
#endif
return true;
}
@@ -240,34 +351,41 @@ public:
if (btn.pressed) {
btn.pressed = false;
trigger_button(btn.dik, false);
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
if (btn.id == kAutoHuntButton && !auto_hunt_long_pressed_)
PythonBoot::AutoHuntToggle();
#endif
}
btn.finger_id = -1;
return true;
}
}
// 4. Pinch end
if (finger_id == pinch_finger1_ || finger_id == pinch_finger2_) {
pinch_finger1_ = -1;
pinch_finger2_ = -1;
// 4. Pinch end: the finger left down keeps looking (never a tap).
if (pinch_finger_id_ >= 0 && (finger_id == pinch_finger_id_ || finger_id == look_finger_id_)) {
if (finger_id == look_finger_id_) {
look_finger_id_ = pinch_finger_id_;
look_last_x_ = pinch_x_;
look_last_y_ = pinch_y_;
}
pinch_finger_id_ = -1;
pinch_last_dist_ = 0.0f;
if (finger_id == camera_finger_id_) camera_finger_id_ = -1;
look_rotating_ = look_can_rotate_;
look_moved_ = true;
return true;
}
// 5. Camera finger release
if (finger_id == camera_finger_id_) {
camera_finger_id_ = -1;
if (camera_dragged_) {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::CameraEndDrag();
#endif
} else if ((get_time_sec() - camera_down_time_) < 0.35) {
// Short tap on 3D world: select target (mob, NPC, ground)
// 5. Look finger release; a short still touch selects a target (mob, NPC, ground).
if (finger_id == look_finger_id_) {
look_finger_id_ = -1;
look_rotating_ = false;
keyboard_requested_ = false;
if (!look_moved_ && (get_time_sec() - look_down_time_) < kTapMaxSec && tap_stage_ == 0) {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::UIMouseMove(int(px), int(py));
PythonBoot::UIMouseButton(1, true, int(px), int(py));
PythonBoot::UIMouseButton(1, false, int(px), int(py));
tap_x_ = px;
tap_y_ = py;
tap_stage_ = 1;
#endif
}
return true;
@@ -290,9 +408,15 @@ public:
const float r = btn.radius;
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
if (btn.id >= 2) {
const std::string skill_icon = PythonBoot::LocalQuickSlotSkillIcon(btn.id - 2);
if (!skill_icon.empty()) {
if (btn.id == kAutoHuntButton) {
draw_auto_hunt_button(commands, btn);
continue;
}
if (is_quick_slot_button(btn.id)) {
std::string skill_icon;
float uv[4];
int count = 0;
if (PythonBoot::LocalQuickSlotIcon(btn.id - 2, &skill_icon, uv, &count)) {
UIRenderCommand icon{};
icon.kind = UIRenderCommand::Image;
icon.x1 = btn.x - r * 0.68f;
@@ -301,7 +425,17 @@ public:
icon.y2 = btn.y + r * 0.68f;
icon.argb = icon_col;
icon.text = skill_icon;
icon.su = uv[0];
icon.sv = uv[1];
icon.eu = uv[2];
icon.ev = uv[3];
commands.push_back(std::move(icon));
if (count > 0) {
const std::string digits = std::to_string(std::min(count, 9999));
const float h = r * 0.26f;
draw_segment_text(commands, digits, btn.x + r * 0.62f - segment_text_width(digits, h),
btn.y + r * 0.62f - h, h);
}
continue;
}
}
@@ -409,12 +543,48 @@ public:
if (joystick_active_ && joystick_finger_id_ == 101) {
handled |= on_finger_motion(101, norm_x, norm_y);
}
if (camera_finger_id_ == 101) {
if (look_finger_id_ == 101) {
handled |= on_finger_motion(101, norm_x, norm_y);
}
return handled;
}
// Seven-segment text (digits, 'F', 'P', 'S', ' ') with its top-left at (left, top), each glyph h
// tall, on a dark backing. Used for quick-slot counts and the test-build FPS readout.
static void draw_segment_text(std::vector<UIRenderCommand>& commands, const std::string& text,
float left, float top, float h, std::uint32_t color = 0xFFFFFFFF) {
static constexpr std::uint8_t kDigits[10] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};
const float w = h * 0.55f, gap = h * 0.3f;
UIRenderCommand back{};
back.kind = UIRenderCommand::Bar;
back.x1 = left - 3.0f;
back.y1 = top - 3.0f;
back.x2 = left + segment_text_width(text, h) + 3.0f;
back.y2 = top + h + 3.0f;
back.argb = 0xA0000000;
commands.push_back(back);
float x = left;
for (char c : text) {
std::uint8_t seg = 0;
if (c >= '0' && c <= '9') seg = kDigits[c - '0'];
else if (c == 'F') seg = 0x71;
else if (c == 'P') seg = 0x73;
else if (c == 'S') seg = 0x6D;
const float t = top, m = top + h * 0.5f, b = top + h;
if (seg & 0x01) draw_line(commands, x, t, x + w, t, color);
if (seg & 0x02) draw_line(commands, x + w, t, x + w, m, color);
if (seg & 0x04) draw_line(commands, x + w, m, x + w, b, color);
if (seg & 0x08) draw_line(commands, x, b, x + w, b, color);
if (seg & 0x10) draw_line(commands, x, m, x, b, color);
if (seg & 0x20) draw_line(commands, x, t, x, m, color);
if (seg & 0x40) draw_line(commands, x, m, x + w, m, color);
x += w + gap;
}
}
static float segment_text_width(const std::string& text, float h) {
return text.empty() ? 0.0f : float(text.size()) * h * 0.85f - h * 0.3f;
}
private:
bool gameplay_controls_visible() const {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
@@ -440,18 +610,47 @@ private:
btn.pressed = false;
btn.finger_id = -1;
}
if (camera_dragged_) {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::CameraEndDrag();
#endif
}
camera_finger_id_ = -1;
camera_dragged_ = false;
pinch_finger1_ = -1;
pinch_finger2_ = -1;
look_finger_id_ = -1;
look_rotating_ = false;
pinch_finger_id_ = -1;
pinch_last_dist_ = 0.0f;
}
static constexpr int kAutoHuntButton = 8;
static bool is_quick_slot_button(int id) { return id >= 2 && id <= 7; }
// A circular arrow; green while the hunt runs.
static void draw_auto_hunt_button(std::vector<UIRenderCommand>& commands, const ButtonDef& btn) {
bool on = false;
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
on = PythonBoot::AutoHuntIsEnabled();
#endif
const uint32_t disc = btn.pressed ? btn.color_pressed : (on ? 0xC040C040 : btn.color_idle);
draw_filled_disc(commands, btn.x, btn.y, btn.radius, disc);
const uint32_t col = on ? 0xFFB0FFB0 : 0xDDFFFFFF;
const float r = btn.radius * 0.45f;
const float kPi = 3.14159265f;
const int segments = 12;
const float a0 = -kPi * 0.35f, a1 = a0 + kPi * 1.6f;
for (int i = 0; i < segments; ++i) {
const float t0 = a0 + (a1 - a0) * float(i) / float(segments);
const float t1 = a0 + (a1 - a0) * float(i + 1) / float(segments);
draw_line(commands, btn.x + std::cos(t0) * r, btn.y + std::sin(t0) * r,
btn.x + std::cos(t1) * r, btn.y + std::sin(t1) * r, col);
}
// Arrowhead at the arc's end, pointing along the direction of travel.
const float ex = btn.x + std::cos(a1) * r, ey = btn.y + std::sin(a1) * r;
const float tx = -std::sin(a1), ty = std::cos(a1);
const float nx = std::cos(a1), ny = std::sin(a1);
const float h = r * 0.55f;
draw_line(commands, ex, ey, ex - tx * h + nx * h * 0.6f, ey - ty * h + ny * h * 0.6f, col);
draw_line(commands, ex, ey, ex - tx * h - nx * h * 0.6f, ey - ty * h - ny * h * 0.6f, col);
if (on) {
// A play dot in the middle while running.
draw_rect_bar(commands, btn.x - 2.0f, btn.y - 2.0f, btn.x + 2.0f, btn.y + 2.0f, col);
}
}
void init_buttons() {
buttons_.clear();
// Lower-right combat controls. All menus and status UI remain the original 40250 UI.
@@ -462,10 +661,11 @@ private:
buttons_.push_back({5, 0x05, 0, 0, 26.0f, "S4", 0x80906030, 0xD0D08050});
buttons_.push_back({6, 0x06, 0, 0, 26.0f, "S5", 0x80603090, 0xD08050D0});
buttons_.push_back({7, 0x07, 0, 0, 26.0f, "S6", 0x80308090, 0xD050C0D0});
buttons_.push_back({kAutoHuntButton, 0, 0, 0, 24.0f, "AUTO", 0x80707070, 0xD0FFFFFF});
}
void layout_buttons() {
const float W = float(screen_w_);
const float W = float(screen_w_) - safe_inset_;
const float H = float(screen_h_);
for (auto& btn : buttons_) {
switch (btn.id) {
@@ -504,6 +704,11 @@ private:
btn.y = H - 225.0f;
btn.radius = std::min(28.0f, H * 0.08f);
break;
case kAutoHuntButton:
btn.x = W - 75.0f;
btn.y = H - 295.0f;
btn.radius = std::min(24.0f, H * 0.07f);
break;
}
}
}
@@ -623,9 +828,26 @@ private:
bool enabled_ = false;
int screen_w_ = 1280;
int screen_h_ = 720;
float safe_inset_ = 0.0f;
bool controls_were_visible_ = false;
int64_t ui_touch_finger_id_ = -1;
enum class UiGesture { None, Pending, Dragging, LongPressed };
UiGesture ui_gesture_ = UiGesture::None;
float ui_start_x_ = 0.0f;
float ui_start_y_ = 0.0f;
double ui_down_time_ = 0.0;
double last_tap_time_ = -1.0;
float last_tap_x_ = 0.0f;
float last_tap_y_ = 0.0f;
bool last_tap_attached_ = false;
void (*haptic_)() = nullptr;
static constexpr float kUiDragSlop = 10.0f;
static constexpr double kLongPressSec = 0.5;
static constexpr double kDoubleTapSec = 0.35;
static constexpr float kDoubleTapSlop = 24.0f;
bool keyboard_requested_ = false;
unsigned keyboard_serial_ = 0;
bool joystick_active_ = false;
int64_t joystick_finger_id_ = -1;
@@ -637,17 +859,34 @@ private:
float joystick_knob_radius_ = 28.0f;
float move_angle_ = 0.0f;
int64_t camera_finger_id_ = -1;
float camera_start_x_ = 0.0f;
float camera_start_y_ = 0.0f;
float camera_last_x_ = 0.0f;
float camera_last_y_ = 0.0f;
double camera_down_time_ = 0.0;
bool camera_dragged_ = false;
// Zones are fractions of the screen; distances are logical UI pixels.
static constexpr float kMoveZoneRight = 0.5f;
static constexpr float kMoveZoneTop = 0.2f;
static constexpr float kLookDeadZone = 8.0f;
static constexpr float kLookYawDegPerPx = 0.25f;
static constexpr float kLookPitchDegPerPx = 0.15f;
static constexpr double kTapMaxSec = 0.3;
int64_t pinch_finger1_ = -1;
int64_t pinch_finger2_ = -1;
int64_t look_finger_id_ = -1;
bool look_can_rotate_ = false;
bool look_rotating_ = false;
bool look_moved_ = false;
float look_start_x_ = 0.0f;
float look_start_y_ = 0.0f;
float look_last_x_ = 0.0f;
float look_last_y_ = 0.0f;
double look_down_time_ = 0.0;
int64_t pinch_finger_id_ = -1;
float pinch_x_ = 0.0f;
float pinch_y_ = 0.0f;
float pinch_last_dist_ = 0.0f;
int tap_stage_ = 0; // 1: press next frame, 2: release next frame
float tap_x_ = 0.0f;
float tap_y_ = 0.0f;
std::vector<ButtonDef> buttons_;
double auto_hunt_down_time_ = 0.0;
bool auto_hunt_long_pressed_ = false;
};
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
#
# PROVIDE: mt_register
# REQUIRE: LOGIN mysql
# KEYWORD: shutdown
#
# Account registration service for the 40250 server (tools/40250/register_server.py).
# Install: /usr/local/etc/rc.d/mt_register, then `sysrc mt_register_enable=YES`.
. /etc/rc.subr
name=mt_register
rcvar=mt_register_enable
load_rc_config $name
: ${mt_register_enable:=NO}
: ${mt_register_dir:=/usr/metin2/server/register}
pidfile=/var/run/${name}.pid
procname=/usr/local/bin/python3.9
command=/usr/sbin/daemon
command_args="-f -p ${pidfile} -o ${mt_register_dir}/register.log ${procname} ${mt_register_dir}/register_server.py"
run_rc_command "$1"
+130
View File
@@ -0,0 +1,130 @@
#!/usr/local/bin/python3.9
"""Account registration service for the 40250 server (PORT: 40250 has no in-client registration).
POST /register with a form body `login=..&password=..&social_id=..` inserts a row into
account.account the same way the existing accounts are stored (password = MySQL PASSWORD()).
The reply is one plain-text line the client shows directly: `OK` or `ERR <CODE>`, where CODE is
BAD_LOGIN, BAD_PASSWORD, BAD_SOCIAL_ID, EXISTS, RATE_LIMIT, BAD_REQUEST or SERVER.
Deployed to /usr/metin2/server/register/ and started by /usr/local/etc/rc.d/mt_register.
"""
import argparse
import re
import subprocess
import sys
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs
LOGIN_RE = re.compile(r"^[A-Za-z0-9]{4,16}$") # LoginWindow ID_EditLine input_limit 16
PASSWORD_RE = re.compile(r"^[\x21-\x7e]{4,16}$") # printable ASCII, Password_EditLine input_limit 16
SOCIAL_ID_RE = re.compile(r"^[0-9]{7}$") # the delete code introselect asks for
RATE_WINDOW = 3600
RATE_MAX = 5 # successful registrations per IP per hour
MAX_BODY = 1024
_rate_lock = threading.Lock()
_rate = {} # ip -> [timestamps of successful registrations]
def log(msg):
sys.stdout.write(time.strftime("%Y-%m-%d %H:%M:%S ") + msg + "\n")
sys.stdout.flush()
def rate_limited(ip):
now = time.time()
with _rate_lock:
hits = [t for t in _rate.get(ip, []) if now - t < RATE_WINDOW]
_rate[ip] = hits
return len(hits) >= RATE_MAX
def rate_record(ip):
with _rate_lock:
_rate.setdefault(ip, []).append(time.time())
def create_account(login, password, social_id):
# Every value is validated above; the password additionally travels as hex so no quoting
# can leak into the statement.
sql = ("INSERT INTO account.account (login, password, social_id, status, create_time, last_play) "
"VALUES ('%s', PASSWORD(UNHEX('%s')), '%s', 'OK', NOW(), NOW())"
% (login, password.encode("ascii").hex(), social_id))
r = subprocess.run(["/usr/local/bin/mysql", "-uroot", "-N", "-e", sql], capture_output=True, text=True, timeout=10)
if r.returncode == 0:
return "OK"
if "Duplicate entry" in r.stderr or "ERROR 1062" in r.stderr:
return "ERR EXISTS"
log("mysql error: " + r.stderr.strip())
return "ERR SERVER"
class Handler(BaseHTTPRequestHandler):
server_version = "mt-register/1"
def log_message(self, fmt, *args):
pass
def reply(self, code, text):
body = (text + "\n").encode("ascii")
self.send_response(code)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/ping":
self.reply(200, "OK")
else:
self.reply(404, "ERR BAD_REQUEST")
def do_POST(self):
ip = self.client_address[0]
if self.path != "/register":
return self.reply(404, "ERR BAD_REQUEST")
try:
n = int(self.headers.get("Content-Length", "0"))
except ValueError:
n = -1
if n < 0 or n > MAX_BODY:
return self.reply(400, "ERR BAD_REQUEST")
try:
form = parse_qs(self.rfile.read(n).decode("ascii"), keep_blank_values=True)
except (UnicodeDecodeError, ValueError):
return self.reply(400, "ERR BAD_REQUEST")
login = form.get("login", [""])[0]
password = form.get("password", [""])[0]
social_id = form.get("social_id", [""])[0]
if not LOGIN_RE.match(login):
result = "ERR BAD_LOGIN"
elif not PASSWORD_RE.match(password):
result = "ERR BAD_PASSWORD"
elif not SOCIAL_ID_RE.match(social_id):
result = "ERR BAD_SOCIAL_ID"
elif rate_limited(ip):
result = "ERR RATE_LIMIT"
else:
result = create_account(login.lower(), password, social_id)
if result == "OK":
rate_record(ip)
log("%s register %s -> %s" % (ip, login, result))
self.reply(200, result)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--bind", default="0.0.0.0")
ap.add_argument("--port", type=int, default=11080)
args = ap.parse_args()
httpd = ThreadingHTTPServer((args.bind, args.port), Handler)
log("listening on %s:%d" % (args.bind, args.port))
httpd.serve_forever()
if __name__ == "__main__":
main()