feat(platform): add native Cocoa/AppKit windowing for macOS (#1559)

New Features:
- Native macOS app with Cocoa windowing, OpenGL view, improved input, clipboard and cursor handling.
- Platform-aware keyboard and shortcut behavior with clearer primary/meta/control semantics and normalized key combos.

Bug Fixes:
- Expanded detection for "connection refused" network errors.

Chores:
- macOS build/bundle and packaging improvements; new dependency overlay added; CI now runs macOS build when relevant.

Tests:
- AddressSanitizer support added for test builds.
This commit is contained in:
Luan Santos 2026-05-16 09:06:48 -07:00 committed by GitHub
parent 6a478408ee
commit 388b3ba2ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 2648 additions and 342 deletions

View file

@ -75,6 +75,7 @@ jobs:
contents: read
outputs:
compile: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') && 'true' || steps.filter.outputs.compile }}
macos: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') && 'true' || steps.filter.outputs.macos }}
android: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') && 'true' || steps.filter.outputs.android }}
docker: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') && 'true' || steps.filter.outputs.docker }}
browser: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') && 'true' || steps.filter.outputs.browser }}
@ -105,6 +106,14 @@ jobs:
- ".github/workflows/reusable-build-windows.yml"
- ".github/workflows/reusable-checks.yml"
- ".github/workflows/reusable-tests-lua.yml"
macos:
- "src/**"
- "cmake/**"
- "vcpkg.json"
- "CMakeLists.txt"
- "CMakePresets.json"
- ".github/workflows/ci.yml"
- ".github/workflows/reusable-build-macos.yml"
android:
- "src/**"
- "android/**"
@ -176,6 +185,16 @@ jobs:
uses: ./.github/workflows/reusable-build-windows.yml
secrets: inherit
build-macos:
name: Build - macOS
needs: [changes, checks, tests-lua]
if: needs.changes.outputs.macos == 'true' && needs.checks.result == 'success' && needs.tests-lua.result == 'success' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false)
permissions:
contents: read
pull-requests: read
uses: ./.github/workflows/reusable-build-macos.yml
secrets: inherit
build-android-apk:
name: Build - Android APK
needs: [changes, checks, tests-lua]

View file

@ -0,0 +1,137 @@
name: Reusable macOS Build
on:
workflow_call:
permissions:
contents: read
pull-requests: read
env:
LUKKA_RUN_VCPKG_SHA: "${{ vars.LUKKA_RUN_VCPKG_SHA != '' && vars.LUKKA_RUN_VCPKG_SHA || 'b3dd708d38df5c856fe1c18dc0d59ab771f93921' }}"
CMAKE_BUILD_PARALLEL_LEVEL: 2
MAKEFLAGS: "-j 2"
jobs:
build:
if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }}
name: ${{ matrix.os }}-${{ matrix.buildtype }}
runs-on: ${{ matrix.os }}
concurrency:
group: otclient-${{ github.workflow }}-${{ github.ref }}-${{ matrix.buildtype }}
cancel-in-progress: true
strategy:
fail-fast: false
matrix:
os: [macos-15]
buildtype: [macos-release]
include:
- os: macos-15
triplet: arm64-osx
cmake_build_type: Release
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get vcpkg commit ID
id: vcpkg-step
run: |
vcpkgCommitId=$(grep '.builtin-baseline' vcpkg.json | awk -F: '{print $2}' | tr -d '," ')
echo "vcpkgGitCommitId=$vcpkgCommitId" >> $GITHUB_OUTPUT
- name: Compute vcpkg.json hash
id: hash
run: |
hash=$(shasum -a 256 vcpkg.json | awk '{print toupper($1)}')
echo "hash=$hash" >> $GITHUB_OUTPUT
- name: Cache vcpkg binary artifacts
uses: actions/cache@v4
with:
path: |
~/.cache/vcpkg/archives
${{ github.workspace }}/vcpkg/downloads
${{ github.workspace }}/vcpkg/installed
${{ github.workspace }}/vcpkg/buildtrees
key: vcpkg-${{ matrix.os }}-${{ matrix.buildtype }}-${{ steps.hash.outputs.hash }}
restore-keys: |
vcpkg-${{ matrix.buildtype }}-
- name: Setup Python
id: python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Setup vcpkg
uses: lukka/run-vcpkg@a400452f634fe49e9f18d388aeb1809dcc642136
env:
VCPKG_PYTHON3: ${{ steps.python.outputs.python-path }}
with:
vcpkgGitCommitId: ${{ steps.vcpkg-step.outputs.vcpkgGitCommitId }}
vcpkgJsonGlob: 'vcpkg.json'
- name: Validate vcpkg baseline SHA
env:
VCPKG_GIT_COMMIT_ID: ${{ steps.vcpkg-step.outputs.vcpkgGitCommitId }}
run: |
SHAS="${VCPKG_GIT_COMMIT_ID}"
if [ -z "${SHAS}" ]; then
echo "::error title=Missing vcpkg baseline::Provide a full 40-char vcpkgGitCommitId."
exit 1
fi
if ! echo "${SHAS}" | grep -Eq '^[0-9a-f]{40}$'; then
echo "::error title=Invalid vcpkg baseline::vcpkgGitCommitId must be a full 40-char commit SHA."
exit 1
fi
- name: Install CMake and Ninja
uses: lukka/get-cmake@v3.31.6
- name: Cache CMake build directory
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/build-${{ matrix.buildtype }}
key: cmake-dir-${{ matrix.buildtype }}
restore-keys: |
cmake-dir-${{ matrix.buildtype }}
- name: Configure and Build
env:
BUILD_DIR: build-${{ matrix.buildtype }}
CMAKE_BUILD_TYPE: ${{ matrix.cmake_build_type }}
VCPKG_TARGET_TRIPLET: ${{ matrix.triplet }}
run: |
cmake -G Ninja -S . -B "${BUILD_DIR}" \
-DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" \
-DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake \
-DVCPKG_TARGET_TRIPLET="${VCPKG_TARGET_TRIPLET}" \
-DTOGGLE_BIN_FOLDER=ON \
-DOPTIONS_ENABLE_IPO=OFF \
-DCMAKE_CXX_FLAGS_RELEASE="-O2 -DNDEBUG" \
-DCMAKE_C_FLAGS_RELEASE="-O2 -DNDEBUG" \
-DTOGGLE_BOT_PROTECTION=OFF
cmake --build "${BUILD_DIR}" --target otclient
- name: Create app bundle archive
env:
BUILD_DIR: build-${{ matrix.buildtype }}
ARTIFACT_NAME: otclient-${{ matrix.os }}-${{ matrix.buildtype }}
run: |
cd "${BUILD_DIR}/bin"
xattr -cr OTClient.app
codesign --force --deep --sign - OTClient.app
ditto -c -k --keepParent OTClient.app "${ARTIFACT_NAME}.zip"
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: otclient-${{ matrix.os }}-${{ matrix.buildtype }}-${{ github.sha }}
path: build-${{ matrix.buildtype }}/bin/otclient-${{ matrix.os }}-${{ matrix.buildtype }}.zip
if-no-files-found: error

1
.gitignore vendored
View file

@ -196,6 +196,7 @@ pip-log.txt
#################
/otclient
/OTClient.app
/modules/otclientrc.lua
src/framework/graphics/dx/

View file

@ -138,7 +138,9 @@
"BUILD_STATIC_LIBRARY": "ON",
"CMAKE_BUILD_TYPE": "Release",
"OPTIONS_ENABLE_SCCACHE": "ON",
"CMAKE_CXX_FLAGS_RELEASE": "-O1 -DNDEBUG",
"OPTIONS_ENABLE_IPO": "OFF",
"CMAKE_CXX_FLAGS_RELEASE": "-O2 -DNDEBUG",
"CMAKE_C_FLAGS_RELEASE": "-O2 -DNDEBUG",
"VCPKG_TARGET_TRIPLET": "arm64-osx",
"VCPKG_HOST_TRIPLET": "arm64-osx",
"VCPKG_BUILD_TYPE": "release",

32
cmake/Info.plist.in Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>@MACOSX_BUNDLE_DISPLAY_NAME@</string>
<key>CFBundleExecutable</key>
<string>@MACOSX_BUNDLE_EXECUTABLE_NAME@</string>
<key>CFBundleIdentifier</key>
<string>@MACOSX_BUNDLE_IDENTIFIER@</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>@MACOSX_BUNDLE_BUNDLE_NAME@</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>@MACOSX_BUNDLE_SHORT_VERSION_STRING@</string>
<key>CFBundleVersion</key>
<string>@MACOSX_BUNDLE_VERSION@</string>
<key>CFBundleIconFile</key>
<string>@MACOSX_BUNDLE_ICON_FILE@</string>
<key>LSMinimumSystemVersion</key>
<string>@MACOSX_BUNDLE_MINIMUM_SYSTEM_VERSION@</string>
<key>NSHumanReadableCopyright</key>
<string>@MACOSX_BUNDLE_COPYRIGHT@</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

BIN
cmake/OTClient.icns Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 510 KiB

Before After
Before After

View file

@ -43,6 +43,8 @@ KeyboardShiftModifier = 4
KeyboardCtrlShiftModifier = 5
KeyboardAltShiftModifier = 6
KeyboardCtrlAltShiftModifier = 7
KeyboardMetaModifier = 8
KeyboardPrimaryModifier = 16
MouseNoButton = 0
MouseLeftButton = 1
@ -90,9 +92,10 @@ KeyRight = 17
KeyNumLock = 18
KeyScrollLock = 19
KeyCapsLock = 20
KeyCtrl = 21
KeyCtrlCmd = 21
KeyShift = 22
KeyAlt = 23
KeyAltOpt = 23
KeyControl = 24
KeyMeta = 25
KeyMenu = 26
KeySpace = 32 -- ' '
@ -217,9 +220,10 @@ KeyCodeDescs = {
[KeyNumLock] = 'NumLock',
[KeyScrollLock] = 'ScrollLock',
[KeyCapsLock] = 'CapsLock',
[KeyCtrl] = 'Ctrl',
[KeyCtrlCmd] = 'Ctrl',
[KeyShift] = 'Shift',
[KeyAlt] = 'Alt',
[KeyAltOpt] = 'Alt',
[KeyControl] = 'Control',
[KeyMeta] = 'Meta',
[KeyMenu] = 'Menu',
[KeySpace] = 'Space',
@ -358,3 +362,23 @@ DisplayInherit = 21
EVENT_TEXT_NONE = 0
EVENT_TEXT_CLICK = 1
EVENT_TEXT_HOVER = 2
function initPlatformKeyDescs()
local platformType = g_window.getPlatformType()
local isMacOS = platformType:find("MACOS") ~= nil
local isWindows = platformType:find("WIN32") ~= nil
local isX11 = platformType:find("X11") ~= nil
if isMacOS then
KeyCodeDescs[KeyCtrlCmd] = 'Cmd'
KeyCodeDescs[KeyAltOpt] = 'Option'
KeyCodeDescs[KeyControl] = 'Ctrl'
KeyCodeDescs[KeyMeta] = 'Cmd'
elseif isWindows then
KeyCodeDescs[KeyControl] = 'Ctrl'
KeyCodeDescs[KeyMeta] = 'Win'
elseif isX11 then
KeyCodeDescs[KeyControl] = 'Ctrl'
KeyCodeDescs[KeyMeta] = 'Super'
end
end

View file

@ -34,5 +34,6 @@ Module
dofile 'objectpool'
Keybind.init()
initPlatformKeyDescs()
@onUnload: Keybind.terminate()

View file

@ -534,19 +534,27 @@ function Keybind.setPrimaryActionKey(category, action, preset, keyCombo, chatMod
local index = category .. '_' .. action
local keybind = Keybind.defaultKeybinds[index]
if keyCombo and type(keyCombo) == "string" and keyCombo:len() > 0 then
keyCombo = retranslateKeyComboDesc(keyCombo)
end
local numericChatMode = chatMode
chatMode = tostring(chatMode)
local keys = Keybind.configs.keybinds[preset]:getNode(index)
if not keys then
keys = table.recursivecopy(keybind.keys)
else
chatMode = tostring(chatMode)
keys = {}
for k, v in pairs(keybind.keys) do
keys[tostring(k)] = table.recursivecopy(v)
end
end
if keybind.callbacks then
Keybind.unbind(category, action)
end
if not keys[chatMode] then
keys[chatMode] = { primary = keyCombo, secondary = keybind.keys[tonumber(chatMode)].secondary }
keys[chatMode] = { primary = keyCombo, secondary = keybind.keys[numericChatMode].secondary }
end
keys[chatMode].primary = keyCombo
@ -570,19 +578,27 @@ function Keybind.setSecondaryActionKey(category, action, preset, keyCombo, chatM
local index = category .. '_' .. action
local keybind = Keybind.defaultKeybinds[index]
if keyCombo and type(keyCombo) == "string" and keyCombo:len() > 0 then
keyCombo = retranslateKeyComboDesc(keyCombo)
end
local numericChatMode = chatMode
chatMode = tostring(chatMode)
local keys = Keybind.configs.keybinds[preset]:getNode(index)
if not keys then
keys = table.recursivecopy(keybind.keys)
else
chatMode = tostring(chatMode)
keys = {}
for k, v in pairs(keybind.keys) do
keys[tostring(k)] = table.recursivecopy(v)
end
end
if keybind.callbacks then
Keybind.unbind(category, action)
end
if not keys[chatMode] then
keys[chatMode] = { primary = keybind.keys[tonumber(chatMode)].primary, secondary = keyCombo }
keys[chatMode] = { primary = keybind.keys[numericChatMode].primary, secondary = keyCombo }
end
keys[chatMode].secondary = keyCombo
@ -640,7 +656,7 @@ function Keybind.getKeybindKeys(category, action, chatMode, preset, forceDefault
secondary = keybind.keys[chatMode].secondary
}
else
keys = keys[chatMode] or keys[tostring(chatMode)]
keys = keys[tostring(chatMode)] or keys[chatMode]
end
if not keys then
@ -764,6 +780,18 @@ function Keybind.editHotkeyKeys(hotkeyId, primary, secondary, chatMode)
Keybind.unbindHotkey(hotkeyId, chatMode)
local hotkey = Keybind.hotkeys[chatMode][Keybind.currentPreset][hotkeyId]
if primary ~= nil then
primary = tostring(primary)
if primary:len() > 0 then
primary = retranslateKeyComboDesc(primary)
end
end
if secondary ~= nil then
secondary = tostring(secondary)
if secondary:len() > 0 then
secondary = retranslateKeyComboDesc(secondary)
end
end
hotkey.primary = primary or ""
hotkey.secondary = secondary or ""
Keybind.configs.hotkeys[Keybind.currentPreset]:setNode(chatMode, Keybind.hotkeys[chatMode][Keybind.currentPreset])

View file

@ -1,13 +1,93 @@
-- @docclass
g_keyboard = {}
local function getPlatformFlags()
local platformType = g_window.getPlatformType() or ""
local isMacOS = platformType:find("MACOS") ~= nil
return isMacOS, not isMacOS
end
local function resolveKeyAlias(desc)
if not desc then
return nil
end
local key = desc:trim():lower()
local isMacOS = getPlatformFlags()
if key == 'cmd' or key == 'command' then
return KeyMeta
end
if key == 'primary' then
if isMacOS then
return KeyMeta
end
return KeyCtrlCmd
end
if key == 'ctrl' then
if isMacOS then
return KeyMeta
end
return KeyCtrlCmd
end
if key == 'control' then
return KeyCtrlCmd
end
if key == 'alt' or key == 'option' then
return KeyAltOpt
end
if key == 'meta' or key == 'win' or key == 'super' then
return KeyMeta
end
return nil
end
-- private functions
local function canonicalizeKeyCombo(keyCombo)
if not keyCombo or #keyCombo == 0 then
return keyCombo
end
local hasCtrl = false
local hasMeta = false
local hasAlt = false
local hasShift = false
local mainKey = nil
for _, keyCode in ipairs(keyCombo) do
if keyCode == KeyCtrlCmd or keyCode == KeyControl then
hasCtrl = true
elseif keyCode == KeyMeta then
hasMeta = true
elseif keyCode == KeyAltOpt then
hasAlt = true
elseif keyCode == KeyShift then
hasShift = true
else
mainKey = keyCode
end
end
local combo = {}
if hasCtrl then
table.insert(combo, KeyCtrlCmd)
end
if hasMeta then
table.insert(combo, KeyMeta)
end
if hasAlt then
table.insert(combo, KeyAltOpt)
end
if hasShift then
table.insert(combo, KeyShift)
end
if mainKey then
table.insert(combo, mainKey)
end
return combo
end
function translateKeyCombo(keyCombo)
if not keyCombo or #keyCombo == 0 then
return nil
end
local keyComboDesc = ''
for k, v in pairs(keyCombo) do
for _, v in ipairs(keyCombo) do
local keyDesc = KeyCodeDescs[v]
if keyDesc == nil then
return nil
@ -19,6 +99,10 @@ function translateKeyCombo(keyCombo)
end
local function getKeyCode(key)
local alias = resolveKeyAlias(key)
if alias then
return alias
end
for keyCode, keyDesc in pairs(KeyCodeDescs) do
if keyDesc:lower() == key:trim():lower() then
return keyCode
@ -37,38 +121,51 @@ function retranslateKeyComboDesc(keyComboDesc)
local keyCombo = {}
for i, currentKeyDesc in ipairs(keyComboDesc:split('+')) do
local alias = resolveKeyAlias(currentKeyDesc)
if alias then
table.insert(keyCombo, alias)
else
for keyCode, keyDesc in pairs(KeyCodeDescs) do
if keyDesc:lower() == currentKeyDesc:trim():lower() then
table.insert(keyCombo, keyCode)
end
end
end
end
return translateKeyCombo(keyCombo)
return translateKeyCombo(canonicalizeKeyCombo(keyCombo))
end
function determineKeyComboDesc(keyCode, keyboardModifiers)
local isMacOS = getPlatformFlags()
local keyCombo = {}
if keyCode == KeyCtrl or keyCode == KeyShift or keyCode == KeyAlt then
if keyCode == KeyShift or keyCode == KeyAltOpt or keyCode == KeyMeta or keyCode == KeyCtrlCmd then
table.insert(keyCombo, keyCode)
elseif keyCode == KeyControl then
table.insert(keyCombo, KeyCtrlCmd)
elseif KeyCodeDescs[keyCode] ~= nil then
if keyboardModifiers == KeyboardCtrlModifier then
table.insert(keyCombo, KeyCtrl)
elseif keyboardModifiers == KeyboardAltModifier then
table.insert(keyCombo, KeyAlt)
elseif keyboardModifiers == KeyboardCtrlAltModifier then
table.insert(keyCombo, KeyCtrl)
table.insert(keyCombo, KeyAlt)
elseif keyboardModifiers == KeyboardShiftModifier then
table.insert(keyCombo, KeyShift)
elseif keyboardModifiers == KeyboardCtrlShiftModifier then
table.insert(keyCombo, KeyCtrl)
table.insert(keyCombo, KeyShift)
elseif keyboardModifiers == KeyboardAltShiftModifier then
table.insert(keyCombo, KeyAlt)
table.insert(keyCombo, KeyShift)
elseif keyboardModifiers == KeyboardCtrlAltShiftModifier then
table.insert(keyCombo, KeyCtrl)
table.insert(keyCombo, KeyAlt)
local primaryPressed = bit.band(keyboardModifiers, KeyboardPrimaryModifier) ~= 0
local ctrlPressed = bit.band(keyboardModifiers, KeyboardCtrlModifier) ~= 0
local metaPressed = bit.band(keyboardModifiers, KeyboardMetaModifier) ~= 0
if isMacOS then
if ctrlPressed then
table.insert(keyCombo, KeyCtrlCmd)
end
if primaryPressed or metaPressed then
table.insert(keyCombo, KeyMeta)
end
else
if ctrlPressed or primaryPressed then
table.insert(keyCombo, KeyCtrlCmd)
end
if metaPressed then
table.insert(keyCombo, KeyMeta)
end
end
if bit.band(keyboardModifiers, KeyboardAltModifier) ~= 0 then
table.insert(keyCombo, KeyAltOpt)
end
if bit.band(keyboardModifiers, KeyboardShiftModifier) ~= 0 then
table.insert(keyCombo, KeyShift)
end
table.insert(keyCombo, keyCode)
@ -80,8 +177,11 @@ local function onWidgetKeyDown(widget, keyCode, keyboardModifiers)
if keyCode == KeyUnknown then
return false
end
local callback = widget.boundAloneKeyDownCombos[determineKeyComboDesc(keyCode, KeyboardNoModifier)]
signalcall(callback, widget, keyCode)
local callback
if keyboardModifiers == KeyboardNoModifier then
callback = widget.boundAloneKeyDownCombos[determineKeyComboDesc(keyCode, KeyboardNoModifier)]
signalcall(callback, widget, keyCode)
end
callback = widget.boundKeyDownCombos[determineKeyComboDesc(keyCode, keyboardModifiers)]
return signalcall(callback, widget, keyCode)
end
@ -258,7 +358,15 @@ function g_keyboard.isCtrlPressed()
if (g_platform.isMobile()) then
return false
else
return bit.band(g_window.getKeyboardModifiers(), KeyboardCtrlModifier) ~= 0
return bit.band(g_window.getKeyboardModifiers(), KeyboardPrimaryModifier) ~= 0
end
end
function g_keyboard.isPrimaryPressed()
if (g_platform.isMobile()) then
return false
else
return bit.band(g_window.getKeyboardModifiers(), KeyboardPrimaryModifier) ~= 0
end
end
@ -277,3 +385,52 @@ function g_keyboard.isShiftPressed()
return bit.band(g_window.getKeyboardModifiers(), KeyboardShiftModifier) ~= 0
end
end
function g_keyboard.isControlPressed()
if (g_platform.isMobile()) then
return false
else
return bit.band(g_window.getKeyboardModifiers(), KeyboardCtrlModifier) ~= 0
end
end
function g_keyboard.isMetaPressed()
if (g_platform.isMobile()) then
return false
else
return bit.band(g_window.getKeyboardModifiers(), KeyboardMetaModifier) ~= 0
end
end
local function hasOnlyModifiers(modifiers, requiredMask, allowedMask)
return bit.band(modifiers, requiredMask) == requiredMask and bit.band(modifiers, allowedMask) == modifiers
end
local function primaryAllowedMask()
local _, primaryIsCtrl = getPlatformFlags()
if primaryIsCtrl then
return bit.bor(KeyboardPrimaryModifier, KeyboardCtrlModifier)
end
return KeyboardPrimaryModifier
end
function g_keyboard.isPrimaryModifierOnly(keyboardModifiers)
if (g_platform.isMobile()) then
return false
end
local allowedMask = primaryAllowedMask()
return hasOnlyModifiers(keyboardModifiers, KeyboardPrimaryModifier, allowedMask)
end
function g_keyboard.isPrimaryShiftModifierOnly(keyboardModifiers)
if (g_platform.isMobile()) then
return false
end
local requiredMask = bit.bor(KeyboardPrimaryModifier, KeyboardShiftModifier)
local allowedMask = requiredMask
local _, primaryIsCtrl = getPlatformFlags()
if primaryIsCtrl then
allowedMask = bit.bor(allowedMask, KeyboardCtrlModifier)
end
return hasOnlyModifiers(keyboardModifiers, requiredMask, allowedMask)
end

View file

@ -1,6 +1,6 @@
function translateNetworkError(errcode, connecting, errdesc)
local text
if errcode == 111 then
if errcode == 111 or errcode == 61 then
text = tr('Connection refused, the server might be offline or restarting.\nPlease try again later.')
elseif errcode == 110 then
text = tr('Connection timed out. Either your network is failing or the server is offline.')

View file

@ -1,24 +1,24 @@
-- Add capitalize function to string library if it doesn't exist
if not string.capitalize then
function string.capitalize(str)
if not str or str == "" or str == nil then
return "Unknown"
end
return str:gsub("(%l)(%w*)", function(first, rest)
return first:upper() .. rest
end)
end
function string.capitalize(str)
if not str or str == "" or str == nil then
return "Unknown"
end
return str:gsub("(%l)(%w*)", function(first, rest)
return first:upper() .. rest
end)
end
end
-- Function to truncate text to a maximum length
local function short_text(text, maxLength)
if not text or text == "" or text == nil then
return "Unknown"
end
if string.len(text) > maxLength then
return text:sub(1, maxLength - 3) .. "..."
end
return text
if not text or text == "" or text == nil then
return "Unknown"
end
if string.len(text) > maxLength then
return text:sub(1, maxLength - 3) .. "..."
end
return text
end
if not BossCooldown then
@ -26,7 +26,7 @@ if not BossCooldown then
launchTime = 0,
lastTick = 0,
sort = 0,
search = '',
search = "",
cooldown = {},
widgets = {},
window = nil,
@ -41,30 +41,30 @@ function BossCooldown.create()
BossCooldown.sort = 0
BossCooldown.lastTick = 0
BossCooldown.search = ''
BossCooldown.search = ""
BossCooldown.cooldown = {}
BossCooldown.widgets = {}
BossCooldown.window = openedWindows['bossButton']
BossCooldown.window = openedWindows["bossButton"]
if not BossCooldown.window then
return
end
local toggleFilterButton = BossCooldown.window:recursiveGetChildById('toggleFilterButton')
local toggleFilterButton = BossCooldown.window:recursiveGetChildById("toggleFilterButton")
if toggleFilterButton then
toggleFilterButton:setVisible(false)
end
local newWindowButton = BossCooldown.window:recursiveGetChildById('newWindowButton')
local newWindowButton = BossCooldown.window:recursiveGetChildById("newWindowButton")
if newWindowButton then
newWindowButton:setVisible(false)
end
local contextMenuButton = BossCooldown.window:recursiveGetChildById('contextMenuButton')
local minimizeButton = BossCooldown.window:recursiveGetChildById('minimizeButton')
local contextMenuButton = BossCooldown.window:recursiveGetChildById("contextMenuButton")
local minimizeButton = BossCooldown.window:recursiveGetChildById("minimizeButton")
if contextMenuButton and minimizeButton then
contextMenuButton:setVisible(true)
contextMenuButton:breakAnchors()
@ -72,15 +72,15 @@ function BossCooldown.create()
contextMenuButton:addAnchor(AnchorRight, minimizeButton:getId(), AnchorLeft)
contextMenuButton:setMarginRight(7)
contextMenuButton:setMarginTop(0)
contextMenuButton.onClick = function(widget, mousePos)
local pos = mousePos or g_window.getMousePosition()
return onBossExtra(pos)
end
end
local lockButton = BossCooldown.window:recursiveGetChildById('lockButton')
local lockButton = BossCooldown.window:recursiveGetChildById("lockButton")
if lockButton and contextMenuButton then
lockButton:setVisible(true)
lockButton:breakAnchors()
@ -90,66 +90,66 @@ function BossCooldown.create()
lockButton:setMarginTop(0)
end
local cl = openedWindows['bossButton']:recursiveGetChildById('clickablePanel')
local cl = openedWindows["bossButton"]:recursiveGetChildById("clickablePanel")
if cl then
cl.onMouseWheel = scrollUIPanel
end
-- Set up the event handlers for the search text
local searchText = BossCooldown.window.contentsPanel.searchText
if searchText then
-- Store original handlers
BossCooldown.originalOnKeyPress = searchText.onKeyPress
BossCooldown.originalOnTextChange = searchText.onTextChange
-- Override onKeyPress to unbind movement keys immediately when typing
searchText.onKeyPress = function(widget, keyCode, keyboardModifiers)
-- Unbind movement keys when user starts typing
local gameWalk = modules.game_walk
if gameWalk then
gameWalk.unbindWalkKey('W')
gameWalk.unbindWalkKey('D')
gameWalk.unbindWalkKey('S')
gameWalk.unbindWalkKey('A')
gameWalk.unbindWalkKey('E')
gameWalk.unbindWalkKey('Q')
gameWalk.unbindWalkKey('C')
gameWalk.unbindWalkKey('Z')
gameWalk.unbindTurnKey('Ctrl+W')
gameWalk.unbindTurnKey('Ctrl+D')
gameWalk.unbindTurnKey('Ctrl+S')
gameWalk.unbindTurnKey('Ctrl+A')
gameWalk.unbindWalkKey("W")
gameWalk.unbindWalkKey("D")
gameWalk.unbindWalkKey("S")
gameWalk.unbindWalkKey("A")
gameWalk.unbindWalkKey("E")
gameWalk.unbindWalkKey("Q")
gameWalk.unbindWalkKey("C")
gameWalk.unbindWalkKey("Z")
gameWalk.unbindTurnKey("Control+W")
gameWalk.unbindTurnKey("Control+D")
gameWalk.unbindTurnKey("Control+S")
gameWalk.unbindTurnKey("Control+A")
end
-- Handle Escape key to clear focus and restore movement
if keyCode == KeyEscape then
-- Re-bind movement keys
local gameWalk = modules.game_walk
if gameWalk then
gameWalk.bindWalkKey('W', North)
gameWalk.bindWalkKey('D', East)
gameWalk.bindWalkKey('S', South)
gameWalk.bindWalkKey('A', West)
gameWalk.bindWalkKey('E', NorthEast)
gameWalk.bindWalkKey('Q', NorthWest)
gameWalk.bindWalkKey('C', SouthEast)
gameWalk.bindWalkKey('Z', SouthWest)
gameWalk.bindTurnKey('Ctrl+W', North)
gameWalk.bindTurnKey('Ctrl+D', East)
gameWalk.bindTurnKey('Ctrl+S', South)
gameWalk.bindTurnKey('Ctrl+A', West)
gameWalk.bindWalkKey("W", North)
gameWalk.bindWalkKey("D", East)
gameWalk.bindWalkKey("S", South)
gameWalk.bindWalkKey("A", West)
gameWalk.bindWalkKey("E", NorthEast)
gameWalk.bindWalkKey("Q", NorthWest)
gameWalk.bindWalkKey("C", SouthEast)
gameWalk.bindWalkKey("Z", SouthWest)
gameWalk.bindTurnKey("Control+W", North)
gameWalk.bindTurnKey("Control+D", East)
gameWalk.bindTurnKey("Control+S", South)
gameWalk.bindTurnKey("Control+A", West)
end
widget:clearFocus()
return false
end
-- Call original handler if it exists
if BossCooldown.originalOnKeyPress then
return BossCooldown.originalOnKeyPress(widget, keyCode, keyboardModifiers)
end
return false
end
-- Set up focus change handler
searchText.onFocusChange = onBossSearchFocusChange
end
@ -160,7 +160,7 @@ function BossCooldown:reset()
BossCooldown.sort = 0
BossCooldown.lastTick = 0
BossCooldown.search = ''
BossCooldown.search = ""
BossCooldown.cooldown = {}
BossCooldown.widgets = {}
@ -179,7 +179,7 @@ function BossCooldown:checkTicks()
local needUpdate = false
for _, widget in ipairs(self.widgets) do
layout:enableUpdates()
if self.search == '' or string.find(widget.name:lower(), self.search:lower()) then
if self.search == "" or string.find(widget.name:lower(), self.search:lower(), 1, true) then
widget:setVisible(true)
else
widget:setVisible(false)
@ -189,26 +189,26 @@ function BossCooldown:checkTicks()
widget.tick = widget.tick - 1
widget.cooldown = os.time() + widget.tick
if widget.cooldown < os.time() then
widget.cooldown = os.time() + 60*365*60*24
widget.cooldown = os.time() + 60 * 365 * 60 * 24
end
local bossCooldownLabel = widget:recursiveGetChildById('bossNCooldown')
local bossCooldownLabel = widget:recursiveGetChildById("bossNCooldown")
if bossCooldownLabel then
if widget.tick <= 0 then
bossCooldownLabel:setText('No Cooldown')
bossCooldownLabel:setColor('#c0c0c0')
if widget.type ~= 'nocd' then
bossCooldownLabel:setText("No Cooldown")
bossCooldownLabel:setColor("#c0c0c0")
if widget.type ~= "nocd" then
needUpdate = true
widget.cooldown = os.time() + 60*365*60*24
widget.cooldown = os.time() + 60 * 365 * 60 * 24
end
widget.type = 'nocd'
widget.type = "nocd"
elseif widget.tick <= 60 then
bossCooldownLabel:setText(widget.tick ..'s')
bossCooldownLabel:setColor('#ff9854')
if widget.type ~= 'second' then
bossCooldownLabel:setText(widget.tick .. "s")
bossCooldownLabel:setColor("#ff9854")
if widget.type ~= "second" then
needUpdate = true
end
widget.type = 'second'
widget.type = "second"
else
local duration = math.max(1, widget.tick)
local days = math.floor(duration / 86400)
@ -219,11 +219,11 @@ function BossCooldown:checkTicks()
else
bossCooldownLabel:setText(string.format("%02dh %02dmin", hours, minutes))
end
bossCooldownLabel:setColor('#ff9854')
if widget.type ~= 'timed' then
bossCooldownLabel:setColor("#ff9854")
if widget.type ~= "timed" then
needUpdate = true
end
widget.type = 'timed'
widget.type = "timed"
end
end
end
@ -252,11 +252,11 @@ function BossCooldown:updateWindow()
table.sort(BossCooldown.cooldown, function(a, b)
local acd = a.cooldown
if acd < os.time() then
acd = os.time() + 60*365*60*24
acd = os.time() + 60 * 365 * 60 * 24
end
local bcd = b.cooldown
if bcd < os.time() then
bcd = os.time() + 60*365*60*24
bcd = os.time() + 60 * 365 * 60 * 24
end
return acd < bcd
end)
@ -266,16 +266,16 @@ function BossCooldown:updateWindow()
end)
end
contentsPanel.searchText:setText('', false)
contentsPanel.searchText:setText("", false)
local c = 1
for _, info in ipairs(BossCooldown.cooldown) do
local widget = g_ui.createWidget('BossInfo', contentsPanel.bosses)
local creatureWidget = widget:recursiveGetChildById('creature')
local bossNameLabel = widget:recursiveGetChildById('bossName')
local bossCooldownLabel = widget:recursiveGetChildById('bossNCooldown')
local widget = g_ui.createWidget("BossInfo", contentsPanel.bosses)
local creatureWidget = widget:recursiveGetChildById("creature")
local bossNameLabel = widget:recursiveGetChildById("bossName")
local bossCooldownLabel = widget:recursiveGetChildById("bossNCooldown")
if creatureWidget then
if info.outfit then
creatureWidget:setOutfit(info.outfit)
@ -286,19 +286,19 @@ function BossCooldown:updateWindow()
body = 0,
legs = 0,
feet = 0,
addons = 0
addons = 0,
}
creatureWidget:setOutfit(fallbackOutfit)
end
end
if bossNameLabel then
local bossName = info.name
if not bossName or bossName == "" or bossName:trim() == "" then
bossName = "Boss " .. (info.bossId or "Unknown")
end
local displayName = short_text(string.capitalize(bossName), 13)
bossNameLabel:setText(displayName)
widget:setTooltip(string.capitalize(bossName))
@ -314,13 +314,13 @@ function BossCooldown:updateWindow()
local resttime = math.max(0, info.cooldown - os.time())
if bossCooldownLabel then
if resttime <= 0 then
bossCooldownLabel:setText('No Cooldown')
bossCooldownLabel:setColor('#c0c0c0')
widget.type = 'nocd'
bossCooldownLabel:setText("No Cooldown")
bossCooldownLabel:setColor("#c0c0c0")
widget.type = "nocd"
elseif resttime <= 60 then
bossCooldownLabel:setText(resttime ..'s')
bossCooldownLabel:setColor('#ff9854')
widget.type = 'second'
bossCooldownLabel:setText(resttime .. "s")
bossCooldownLabel:setColor("#ff9854")
widget.type = "second"
else
local duration = math.max(1, resttime)
local days = math.floor(duration / 86400)
@ -331,8 +331,8 @@ function BossCooldown:updateWindow()
else
bossCooldownLabel:setText(string.format("%02dh %02dmin", hours, minutes))
end
bossCooldownLabel:setColor('#ff9854')
widget.type = 'timed'
bossCooldownLabel:setColor("#ff9854")
widget.type = "timed"
end
end
@ -350,17 +350,17 @@ end
function BossCooldown:setupCooldown(cooldown)
BossCooldown.cooldown = {}
for i, cooldownData in pairs(cooldown) do
local raceData = g_things.getRaceData(cooldownData.bossRaceId)
local bossEntry = {
bossId = cooldownData.bossRaceId,
cooldown = cooldownData.cooldownTime,
name = raceData and raceData.name or "",
outfit = raceData and raceData.outfit or nil
bossId = cooldownData.bossRaceId,
cooldown = cooldownData.cooldownTime,
name = raceData and raceData.name or "",
outfit = raceData and raceData.outfit or nil,
}
BossCooldown.cooldown[#BossCooldown.cooldown + 1] = bossEntry
end
@ -369,19 +369,19 @@ function BossCooldown:setupCooldown(cooldown)
end
function checkBossSearch(text)
if #text <= 1 then
BossCooldown.search = ''
if not text or #text <= 1 then
BossCooldown.search = ""
else
BossCooldown.search = text
end
-- Immediately apply the search filter
if BossCooldown.window and BossCooldown.window.contentsPanel and BossCooldown.window.contentsPanel.bosses then
local layout = BossCooldown.window.contentsPanel.bosses:getLayout()
if layout then
layout:enableUpdates()
for _, widget in ipairs(BossCooldown.widgets) do
if BossCooldown.search == '' or string.find(widget.name:lower(), BossCooldown.search:lower()) then
if BossCooldown.search == "" or string.find(widget.name:lower(), BossCooldown.search:lower(), 1, true) then
widget:setVisible(true)
else
widget:setVisible(false)
@ -398,41 +398,41 @@ function onBossSearchFocusChange(widget, focused)
-- When gaining focus, unbind movement keys
local gameWalk = modules.game_walk
if gameWalk then
gameWalk.unbindWalkKey('W')
gameWalk.unbindWalkKey('D')
gameWalk.unbindWalkKey('S')
gameWalk.unbindWalkKey('A')
gameWalk.unbindWalkKey('E')
gameWalk.unbindWalkKey('Q')
gameWalk.unbindWalkKey('C')
gameWalk.unbindWalkKey('Z')
gameWalk.unbindTurnKey('Ctrl+W')
gameWalk.unbindTurnKey('Ctrl+D')
gameWalk.unbindTurnKey('Ctrl+S')
gameWalk.unbindTurnKey('Ctrl+A')
gameWalk.unbindWalkKey("W")
gameWalk.unbindWalkKey("D")
gameWalk.unbindWalkKey("S")
gameWalk.unbindWalkKey("A")
gameWalk.unbindWalkKey("E")
gameWalk.unbindWalkKey("Q")
gameWalk.unbindWalkKey("C")
gameWalk.unbindWalkKey("Z")
gameWalk.unbindTurnKey("Control+W")
gameWalk.unbindTurnKey("Control+D")
gameWalk.unbindTurnKey("Control+S")
gameWalk.unbindTurnKey("Control+A")
end
else
-- When losing focus, bind movement keys back
local gameWalk = modules.game_walk
if gameWalk then
gameWalk.bindWalkKey('W', North)
gameWalk.bindWalkKey('D', East)
gameWalk.bindWalkKey('S', South)
gameWalk.bindWalkKey('A', West)
gameWalk.bindWalkKey('E', NorthEast)
gameWalk.bindWalkKey('Q', NorthWest)
gameWalk.bindWalkKey('C', SouthEast)
gameWalk.bindWalkKey('Z', SouthWest)
gameWalk.bindTurnKey('Ctrl+W', North)
gameWalk.bindTurnKey('Ctrl+D', East)
gameWalk.bindTurnKey('Ctrl+S', South)
gameWalk.bindTurnKey('Ctrl+A', West)
gameWalk.bindWalkKey("W", North)
gameWalk.bindWalkKey("D", East)
gameWalk.bindWalkKey("S", South)
gameWalk.bindWalkKey("A", West)
gameWalk.bindWalkKey("E", NorthEast)
gameWalk.bindWalkKey("Q", NorthWest)
gameWalk.bindWalkKey("C", SouthEast)
gameWalk.bindWalkKey("Z", SouthWest)
gameWalk.bindTurnKey("Control+W", North)
gameWalk.bindTurnKey("Control+D", East)
gameWalk.bindTurnKey("Control+S", South)
gameWalk.bindTurnKey("Control+A", West)
end
end
end
function clearSearch()
BossCooldown.search = ''
BossCooldown.window.contentsPanel.searchText:setText('', false)
BossCooldown.search = ""
BossCooldown.window.contentsPanel.searchText:setText("", false)
end
function onBossExtra(mousePosition)
@ -442,18 +442,20 @@ function onBossExtra(mousePosition)
end
local player = g_game.getLocalPlayer()
if not player then return false end
if not player then
return false
end
local sortByCooldown = BossCooldown.sort == 0
local sortByName = BossCooldown.sort == 1
local menu = g_ui.createWidget('PopupMenu')
local menu = g_ui.createWidget("PopupMenu")
menu:setGameMenu(true)
menu:addCheckBox(tr('sort by cooldown'), sortByCooldown, function()
menu:addCheckBox(tr("sort by cooldown"), sortByCooldown, function()
BossCooldown.sort = 0
BossCooldown:updateWindow()
end)
menu:addCheckBox(tr('sort by name'), sortByName, function()
menu:addCheckBox(tr("sort by name"), sortByName, function()
BossCooldown.sort = 1
BossCooldown:updateWindow()
end)
@ -463,13 +465,13 @@ function onBossExtra(mousePosition)
end
function toggleBossCDFocus(visible)
local widget = BossCooldown.window:recursiveGetChildById('clickablePanel')
local widget = BossCooldown.window:recursiveGetChildById("clickablePanel")
if widget and visible then
widget:setPhantom(true)
elseif widget then
widget:setPhantom(false)
widget.onClick = function()
modules.game_interface.toggleInternalFocus();
modules.game_interface.toggleInternalFocus()
toggleBossCDFocus(not visible)
end
end
@ -482,42 +484,51 @@ function toggleBossCDFocus(visible)
-- Immediately unbind movement keys when search becomes active
local gameWalk = modules.game_walk
if gameWalk then
gameWalk.unbindWalkKey('W')
gameWalk.unbindWalkKey('D')
gameWalk.unbindWalkKey('S')
gameWalk.unbindWalkKey('A')
gameWalk.unbindWalkKey('E')
gameWalk.unbindWalkKey('Q')
gameWalk.unbindWalkKey('C')
gameWalk.unbindWalkKey('Z')
gameWalk.unbindTurnKey('Ctrl+W')
gameWalk.unbindTurnKey('Ctrl+D')
gameWalk.unbindTurnKey('Ctrl+S')
gameWalk.unbindTurnKey('Ctrl+A')
gameWalk.unbindWalkKey("W")
gameWalk.unbindWalkKey("D")
gameWalk.unbindWalkKey("S")
gameWalk.unbindWalkKey("A")
gameWalk.unbindWalkKey("E")
gameWalk.unbindWalkKey("Q")
gameWalk.unbindWalkKey("C")
gameWalk.unbindWalkKey("Z")
gameWalk.unbindTurnKey("Control+W")
gameWalk.unbindTurnKey("Control+D")
gameWalk.unbindTurnKey("Control+S")
gameWalk.unbindTurnKey("Control+A")
end
else
-- Re-bind movement keys when search becomes inactive
local gameWalk = modules.game_walk
if gameWalk then
gameWalk.bindWalkKey('W', North)
gameWalk.bindWalkKey('D', East)
gameWalk.bindWalkKey('S', South)
gameWalk.bindWalkKey('A', West)
gameWalk.bindWalkKey('E', NorthEast)
gameWalk.bindWalkKey('Q', NorthWest)
gameWalk.bindWalkKey('C', SouthEast)
gameWalk.bindWalkKey('Z', SouthWest)
gameWalk.bindTurnKey('Ctrl+W', North)
gameWalk.bindTurnKey('Ctrl+D', East)
gameWalk.bindTurnKey('Ctrl+S', South)
gameWalk.bindTurnKey('Ctrl+A', West)
gameWalk.bindWalkKey("W", North)
gameWalk.bindWalkKey("D", East)
gameWalk.bindWalkKey("S", South)
gameWalk.bindWalkKey("A", West)
gameWalk.bindWalkKey("E", NorthEast)
gameWalk.bindWalkKey("Q", NorthWest)
gameWalk.bindWalkKey("C", SouthEast)
gameWalk.bindWalkKey("Z", SouthWest)
gameWalk.bindTurnKey("Control+W", North)
gameWalk.bindTurnKey("Control+D", East)
gameWalk.bindTurnKey("Control+S", South)
gameWalk.bindTurnKey("Control+A", West)
end
BossCooldown.window:setBorderWidth(0)
end
end
function updateBossFocus()
scheduleEvent(function() BossCooldown.window:recursiveGetChildById('miniwindowScrollBar'):setValue(1) end, 1)
scheduleEvent(function()
if not BossCooldown.window then
return
end
local scrollBar = BossCooldown.window:recursiveGetChildById("miniwindowScrollBar")
if not scrollBar then
return
end
scrollBar:setValue(1)
end, 1)
end
function BossCooldown:hasCooldown(raceId)
@ -532,7 +543,7 @@ end
function BossCooldown:getCooldown(raceId)
for _, widget in pairs(BossCooldown.widgets) do
if widget.bossId and widget.bossId == raceId and widget.cooldown and widget.cooldown > os.time() then
local bossCooldownLabel = widget:recursiveGetChildById('bossNCooldown')
local bossCooldownLabel = widget:recursiveGetChildById("bossNCooldown")
if bossCooldownLabel then
return bossCooldownLabel:getText()
end

View file

@ -221,7 +221,7 @@ function consoleController:onInit()
consoleTabBar.onDragLeave = onDragLeave
consoleTabBar.onDragMove = onDragMove
consolePanel.onKeyPress = function(self, keyCode, keyboardModifiers)
if not (keyboardModifiers == KeyboardCtrlModifier and keyCode == KeyC) then
if not (g_keyboard.isPrimaryModifierOnly(keyboardModifiers) and keyCode == KeyC) then
return false
end
@ -377,10 +377,10 @@ local function unbindMovingKeys()
gameWalk.unbindWalkKey('C')
gameWalk.unbindWalkKey('Z')
gameWalk.unbindTurnKey('Ctrl+W')
gameWalk.unbindTurnKey('Ctrl+D')
gameWalk.unbindTurnKey('Ctrl+S')
gameWalk.unbindTurnKey('Ctrl+A')
gameWalk.unbindTurnKey('Control+W')
gameWalk.unbindTurnKey('Control+D')
gameWalk.unbindTurnKey('Control+S')
gameWalk.unbindTurnKey('Control+A')
end
local function bindMovingKeys()
@ -395,10 +395,10 @@ local function bindMovingKeys()
gameWalk.bindWalkKey('C', SouthEast)
gameWalk.bindWalkKey('Z', SouthWest)
gameWalk.bindTurnKey('Ctrl+W', North)
gameWalk.bindTurnKey('Ctrl+D', East)
gameWalk.bindTurnKey('Ctrl+S', South)
gameWalk.bindTurnKey('Ctrl+A', West)
gameWalk.bindTurnKey('Control+W', North)
gameWalk.bindTurnKey('Control+D', East)
gameWalk.bindTurnKey('Control+S', South)
gameWalk.bindTurnKey('Control+A', West)
end
function switchChat(enabled)

View file

@ -978,8 +978,17 @@ function canPerformKeyCombo(keyCombo)
if not modules.game_console.isChatEnabled() then
return true
end
local platformType = g_window.getPlatformType() or ""
local isMacOS = platformType:find("MACOS") ~= nil
if isMacOS then
return string.match(keyCombo, "Cmd%+") or
string.match(keyCombo, "Ctrl%+") or
string.match(keyCombo, "Alt%+") or
string.match(keyCombo, "Option%+") or
string.match(keyCombo, "F%d+")
end
return string.match(keyCombo, "Ctrl%+") or
string.match(keyCombo, "Alt%+") or
string.match(keyCombo, "Alt%+") or
string.match(keyCombo, "F%d+")
end

View file

@ -1164,7 +1164,7 @@ function processMouseAction(menuPosition, mouseButton, autoWalkPos, lookThing, u
(mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then
g_game.look(lookThing)
return true
elseif useThing and keyboardModifiers == KeyboardCtrlModifier and
elseif useThing and g_keyboard.isPrimaryModifierOnly(keyboardModifiers) and
(mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then
local smartLeftClick = modules.client_options.getOption('smartLeftClick')
@ -1195,7 +1195,7 @@ function processMouseAction(menuPosition, mouseButton, autoWalkPos, lookThing, u
end
end
return true
elseif useThing and useThing:isContainer() and keyboardModifiers == KeyboardCtrlShiftModifier and
elseif useThing and useThing:isContainer() and g_keyboard.isPrimaryShiftModifierOnly(keyboardModifiers) and
(mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then
g_game.open(useThing)
return true
@ -1442,7 +1442,7 @@ function processMouseAction(menuPosition, mouseButton, autoWalkPos, lookThing, u
end
-- Common key combinations for all Classic Control modes
if useThing and useThing:isContainer() and keyboardModifiers == KeyboardCtrlShiftModifier and
if useThing and useThing:isContainer() and g_keyboard.isPrimaryShiftModifierOnly(keyboardModifiers) and
(mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then
g_game.open(useThing)
return true
@ -1454,7 +1454,7 @@ function processMouseAction(menuPosition, mouseButton, autoWalkPos, lookThing, u
(g_mouse.isPressed(MouseRightButton) and mouseButton == MouseLeftButton)) then
g_game.look(lookThing)
return true
elseif useThing and keyboardModifiers == KeyboardCtrlModifier and
elseif useThing and g_keyboard.isPrimaryModifierOnly(keyboardModifiers) and
(mouseButton == MouseLeftButton or mouseButton == MouseRightButton) then
createThingMenu(menuPosition, lookThing, useThing, creatureThing)
return true

View file

@ -23,10 +23,10 @@ local keys = {
}
local turnKeys = {
{ "Ctrl+Up", North },
{ "Ctrl+Right", East },
{ "Ctrl+Down", South },
{ "Ctrl+Left", West },
{ "Control+Up", North },
{ "Control+Right", East },
{ "Control+Down", South },
{ "Control+Left", West },
}
WalkController = Controller:new()

View file

@ -256,9 +256,9 @@ function UIMinimap:onMouseWheel(mousePos, direction)
self:zoomIn()
elseif direction == MouseWheelDown and keyboardModifiers == KeyboardNoModifier then
self:zoomOut()
elseif direction == MouseWheelDown and keyboardModifiers == KeyboardCtrlModifier then
elseif direction == MouseWheelDown and g_keyboard.isPrimaryModifierOnly(keyboardModifiers) then
self:floorUp(1)
elseif direction == MouseWheelUp and keyboardModifiers == KeyboardCtrlModifier then
elseif direction == MouseWheelUp and g_keyboard.isPrimaryModifierOnly(keyboardModifiers) then
self:floorDown(1)
end
end

View file

@ -0,0 +1,51 @@
cmake_minimum_required(VERSION 3.16)
project(inih LANGUAGES C CXX)
option(INIH_WITH_INI_READER "Build C++ INIReader library" OFF)
option(INIH_WITH_DEBUG "Enable debug features" OFF)
add_library(inih ini.c)
set_target_properties(inih PROPERTIES OUTPUT_NAME inih)
target_include_directories(inih
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>
)
if(INIH_WITH_DEBUG)
target_compile_definitions(inih PUBLIC INI_DEBUG)
endif()
if(INIH_WITH_INI_READER)
add_library(INIReader cpp/INIReader.cpp)
set_target_properties(INIReader PROPERTIES OUTPUT_NAME INIReader)
target_compile_features(INIReader PUBLIC cxx_std_11)
target_include_directories(INIReader
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/cpp>
$<INSTALL_INTERFACE:include>
)
target_link_libraries(INIReader PUBLIC inih)
endif()
include(GNUInstallDirs)
install(TARGETS inih
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
if(INIH_WITH_INI_READER)
install(TARGETS INIReader
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
endif()
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/ini.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
if(INIH_WITH_INI_READER)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cpp/INIReader.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
endif()

View file

@ -0,0 +1,62 @@
vcpkg_check_linkage(ONLY_STATIC_LIBRARY)
vcpkg_from_github(
OUT_SOURCE_PATH SOURCE_PATH
REPO benhoyt/inih
REF "r${VERSION}"
SHA512 d69f488299c1896e87ddd3dd20cd9db5848da7afa4c6159b8a99ba9a5d33f35cadfdb9f65d6f2fe31decdbadb8b43bf610ff2699df475e1f9ff045e343ac26ae
HEAD_REF master
)
vcpkg_check_features(
OUT_FEATURE_OPTIONS FEATURE_OPTIONS
FEATURES
cpp INIH_WITH_INI_READER
)
set(INIH_WITH_INI_READER OFF)
if(DEFINED VCPKG_FEATURES)
if("cpp" IN_LIST VCPKG_FEATURES)
set(INIH_WITH_INI_READER ON)
endif()
else()
# Older vcpkg baselines may not populate VCPKG_FEATURES, but we always need INIReader.
set(INIH_WITH_INI_READER ON)
endif()
set(FEATURE_OPTIONS "")
if(INIH_WITH_INI_READER)
list(APPEND FEATURE_OPTIONS "-DINIH_WITH_INI_READER=ON")
endif()
if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug")
set(INIH_CONFIG_DEBUG ON)
else()
set(INIH_CONFIG_DEBUG OFF)
endif()
configure_file(
"${CMAKE_CURRENT_LIST_DIR}/unofficial-inihConfig.cmake.in"
"${CURRENT_PACKAGES_DIR}/share/unofficial-inih/unofficial-inihConfig.cmake"
@ONLY
)
file(COPY "${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt" DESTINATION "${SOURCE_PATH}")
vcpkg_configure_cmake(
SOURCE_PATH "${SOURCE_PATH}"
OPTIONS_DEBUG
${FEATURE_OPTIONS}
"-DINIH_WITH_DEBUG=ON"
OPTIONS_RELEASE
${FEATURE_OPTIONS}
"-DINIH_WITH_DEBUG=OFF"
)
vcpkg_install_cmake()
vcpkg_copy_pdbs()
vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.txt")
configure_file("${CMAKE_CURRENT_LIST_DIR}/usage" "${CURRENT_PACKAGES_DIR}/share/${PORT}/usage" COPYONLY)

View file

@ -0,0 +1,60 @@
if(TARGET unofficial::inih::libinih)
return()
endif()
set(INIH_WITH_INI_READER @INIH_WITH_INI_READER@)
set(INIH_WITH_DEBUG @INIH_CONFIG_DEBUG@)
# Compute the installation prefix relative to this file.
get_filename_component(_IMPORT_PREFIX "${CMAKE_CURRENT_LIST_FILE}" PATH)
get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH)
get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH)
###################
#### libinih ####
add_library(unofficial::inih::libinih UNKNOWN IMPORTED)
find_library(INIH_INIHLIB_RELEASE NAMES inih PATHS "${_IMPORT_PREFIX}/lib/" REQUIRED NO_DEFAULT_PATH)
set_target_properties(unofficial::inih::libinih PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
IMPORTED_LINK_INTERFACE_LANGUAGES "C"
IMPORTED_LOCATION_RELEASE "${INIH_INIHLIB_RELEASE}"
IMPORTED_CONFIGURATIONS "RELEASE"
)
if(INIH_WITH_DEBUG)
set_property(TARGET unofficial::inih::libinih APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
find_library(INIH_INIHLIB_DEBUG NAMES inih PATHS "${_IMPORT_PREFIX}/debug/lib/" REQUIRED NO_DEFAULT_PATH)
set_target_properties(unofficial::inih::libinih PROPERTIES
IMPORTED_LOCATION_DEBUG "${INIH_INIHLIB_DEBUG}"
)
endif()
#### libinih ####
###################
#### INIReader ####
if(INIH_WITH_INI_READER)
add_library(unofficial::inih::inireader UNKNOWN IMPORTED)
find_library(INIH_INIREADER_RELEASE NAMES INIReader PATHS "${_IMPORT_PREFIX}/lib/" REQUIRED NO_DEFAULT_PATH)
set_target_properties(unofficial::inih::inireader PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
IMPORTED_LINK_INTERFACE_LANGUAGES "C;CXX"
IMPORTED_LOCATION_RELEASE "${INIH_INIREADER_RELEASE}"
INTERFACE_LINK_LIBRARIES "unofficial::inih::libinih"
IMPORTED_CONFIGURATIONS "RELEASE"
)
if(INIH_WITH_DEBUG)
set_property(TARGET unofficial::inih::inireader APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
find_library(INIH_INIREADER_DEBUG NAMES INIReader PATHS "${_IMPORT_PREFIX}/debug/lib/" REQUIRED NO_DEFAULT_PATH)
set_target_properties(unofficial::inih::inireader PROPERTIES
IMPORTED_LOCATION_DEBUG "${INIH_INIREADER_DEBUG}"
)
endif()
endif()
#### INIReader ####
###################

7
overlay-ports/inih/usage Normal file
View file

@ -0,0 +1,7 @@
The package inih provides unofficial CMake targets:
find_package(unofficial-inih CONFIG REQUIRED)
# C API
target_link_libraries(main PRIVATE unofficial::inih::libinih)
# C++ API (Requires "cpp" feature)
target_link_libraries(main PRIVATE unofficial::inih::inireader)
Alternatively, if you are using pkg-config use the name "inih" for the C API and "inireader" for the C++ API

View file

@ -0,0 +1,12 @@
{
"name": "inih",
"version-string": "58",
"description": "Simple .INI file parser",
"homepage": "https://github.com/benhoyt/inih",
"license": "BSD-3-Clause",
"features": {
"cpp": {
"description": "C++ INIReader wrapper"
}
}
}

View file

@ -176,9 +176,15 @@ if(APPLE)
find_library(FOUNDATION Foundation REQUIRED)
find_library(IOKIT IOKit REQUIRED)
endif()
if(UNIX AND NOT ANDROID AND NOT WASM)
if(UNIX AND NOT APPLE AND NOT ANDROID AND NOT WASM)
find_package(X11 REQUIRED)
endif()
if(APPLE AND NOT ANDROID AND NOT WASM)
find_library(COCOA_LIBRARY Cocoa REQUIRED)
find_library(COREVIDEO_LIBRARY CoreVideo REQUIRED)
find_library(QUARTZCORE_LIBRARY QuartzCore REQUIRED)
find_library(OPENGL_FRAMEWORK OpenGL REQUIRED)
endif()
if(WIN32)
find_package(DbgHelp REQUIRED)
endif(WIN32)
@ -430,8 +436,6 @@ if (TOGGLE_FRAMEWORK_GRAPHICS)
framework/graphics/texture.cpp
framework/graphics/texturemanager.cpp
framework/graphics/shadermanager.cpp
framework/platform/win32window.cpp
framework/platform/x11window.cpp
framework/ui/uianchorlayout.cpp
framework/ui/uiboxlayout.cpp
framework/ui/uigridlayout.cpp
@ -484,11 +488,30 @@ if(ANDROID)
)
endif()
if(WIN32)
set(SOURCE_FILES ${SOURCE_FILES} framework/platform/win32window.cpp)
elseif(APPLE AND NOT ANDROID AND NOT WASM)
set(SOURCE_FILES ${SOURCE_FILES}
framework/platform/cocoawindow.mm
framework/platform/cocoaview.mm
)
set_source_files_properties(
framework/net/httplogin.cpp
framework/platform/cocoawindow.mm
framework/platform/cocoaview.mm
PROPERTIES
SKIP_UNITY_BUILD_INCLUSION ON
SKIP_PRECOMPILE_HEADERS ON
)
elseif(UNIX AND NOT ANDROID AND NOT WASM)
set(SOURCE_FILES ${SOURCE_FILES} framework/platform/x11window.cpp)
endif()
add_library(otclient_core STATIC ${SOURCE_FILES})
target_link_libraries(otclient_core PRIVATE unofficial::inih::inireader)
if(NOT MSVC)
if(NOT MSVC AND CMAKE_INTERPROCEDURAL_OPTIMIZATION MATCHES "^(ON|1|TRUE)$")
target_link_options(otclient_core PUBLIC -flto=auto)
endif()
@ -916,7 +939,7 @@ else() # Linux
absl::log_internal_check_op
Threads::Threads
X11::X11
$<$<AND:$<PLATFORM_ID:Linux>,$<NOT:$<BOOL:${ANDROID}>>>:X11::X11>
asio::asio
OpenAL::OpenAL
LibLZMA::LibLZMA
@ -940,6 +963,16 @@ else() # Linux
)
endif()
if(APPLE AND NOT ANDROID AND NOT WASM)
target_link_libraries(otclient_core
PUBLIC
${COCOA_LIBRARY}
${COREVIDEO_LIBRARY}
${QUARTZCORE_LIBRARY}
${OPENGL_FRAMEWORK}
)
endif()
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_options(otclient_core
PRIVATE
@ -948,6 +981,41 @@ else() # Linux
endif()
endif()
if(APPLE AND NOT ANDROID AND NOT WASM)
set(MACOSX_BUNDLE_BUNDLE_NAME "OTClient")
set(MACOSX_BUNDLE_DISPLAY_NAME "OTClient")
set(MACOSX_BUNDLE_IDENTIFIER "com.otclient.otclient")
set(MACOSX_BUNDLE_VERSION "${VERSION}")
set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${VERSION}")
set(MACOSX_BUNDLE_COPYRIGHT "Copyright © 2024")
set(MACOSX_BUNDLE_ICON_FILE "OTClient")
if(DEFINED CMAKE_OSX_DEPLOYMENT_TARGET)
set(MACOSX_BUNDLE_MINIMUM_SYSTEM_VERSION "${CMAKE_OSX_DEPLOYMENT_TARGET}")
else()
set(MACOSX_BUNDLE_MINIMUM_SYSTEM_VERSION "11.0")
endif()
set(OTCLIENT_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/Info.plist")
configure_file("${CMAKE_SOURCE_DIR}/cmake/Info.plist.in" "${OTCLIENT_INFO_PLIST}" @ONLY)
set_target_properties(${PROJECT_NAME} PROPERTIES
OUTPUT_NAME "OTClient"
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_INFO_PLIST "${OTCLIENT_INFO_PLIST}"
MACOSX_BUNDLE_ICON_FILE "OTClient"
)
add_custom_command(TARGET ${PROJECT_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
"${CMAKE_SOURCE_DIR}/cmake/OTClient.icns"
"$<TARGET_FILE_DIR:${PROJECT_NAME}>/../Resources/OTClient.icns"
COMMENT "Copying OTClient.icns to app bundle Resources"
VERBATIM
)
endif()
if(ENABLE_DISCORD_RPC AND NOT ANDROID)
target_link_libraries(otclient_core PUBLIC ${DISCORDRPC_LIBRARY})
endif()

View file

@ -1158,7 +1158,7 @@ void Map::findPathAsync(const Position& start, const Position& goal, const std::
}
}
g_asyncDispatcher.detach_task([=] {
g_asyncDispatcher->detach_task([=] {
const auto ret = g_map.newFindPath(start, goal, visibleNodes);
g_dispatcher.addEvent(std::bind(callback, ret));
});

View file

@ -46,7 +46,7 @@
MapView::MapView() : m_lightView(std::make_unique<LightView>(Size())), m_pool(g_drawPool.get(DrawPoolType::MAP))
{
m_floors.resize(g_gameConfig.getMapMaxZ() + 1);
m_floorThreads.resize(g_asyncDispatcher.get_thread_count());
m_floorThreads.resize(g_asyncDispatcher->get_thread_count());
for (auto& thread : m_floorThreads)
thread.resize(m_floors.size());
@ -391,7 +391,7 @@ void MapView::updateVisibleTiles()
};
if (m_multithreading) {
static const int numThreads = g_asyncDispatcher.get_thread_count();
static const int numThreads = g_asyncDispatcher->get_thread_count();
static BS::multi_future<void> tasks(numThreads);
tasks.clear();
@ -404,7 +404,7 @@ void MapView::updateVisibleTiles()
for (auto& floor : m_floorThreads[i])
floor.cachedVisibleTiles.clear();
tasks.emplace_back(g_asyncDispatcher.submit_task([=, this] {
tasks.emplace_back(g_asyncDispatcher->submit_task([=, this] {
processDiagonalRange(m_floorThreads[i], start, end);
}));
}

View file

@ -54,7 +54,7 @@ void SpriteManager::reload() {
}
void SpriteManager::load() {
m_spritesFiles.resize(g_asyncDispatcher.get_thread_count());
m_spritesFiles.resize(g_asyncDispatcher->get_thread_count());
if (g_app.isLoadingAsyncTexture()) {
for (auto& file : m_spritesFiles)
file = std::make_unique<FileStream_m>(g_resources.openFile(m_lastFileName));

View file

@ -809,7 +809,7 @@ const TexturePtr& ThingType::getTexture(const int animationPhase)
m_loading.store(false, std::memory_order_release);
};
g_asyncDispatcher.detach_task(std::move(action));
g_asyncDispatcher->detach_task(std::move(action));
}
return m_textureNull;

View file

@ -283,9 +283,34 @@ namespace Fw
KeyboardNoModifier = 0,
KeyboardCtrlModifier = 1,
KeyboardAltModifier = 2,
KeyboardShiftModifier = 4
KeyboardShiftModifier = 4,
KeyboardMetaModifier = 8,
KeyboardPrimaryModifier = 16
};
inline bool isPrimaryModifierOnly(const int modifiers)
{
#if defined(__APPLE__)
const int primaryMask = KeyboardPrimaryModifier;
#else
const int primaryMask = KeyboardPrimaryModifier | KeyboardCtrlModifier;
#endif
return (modifiers & primaryMask) == primaryMask &&
(modifiers & ~primaryMask) == 0;
}
inline bool isPrimaryShiftModifierOnly(const int modifiers)
{
#if defined(__APPLE__)
const int primaryMask = KeyboardPrimaryModifier;
#else
const int primaryMask = KeyboardPrimaryModifier | KeyboardCtrlModifier;
#endif
const int requiredMask = primaryMask | KeyboardShiftModifier;
return (modifiers & requiredMask) == requiredMask &&
(modifiers & ~requiredMask) == 0;
}
enum WidgetState : int32_t
{
InvalidState = -1,

View file

@ -22,6 +22,7 @@
#include "application.h"
#include "asyncdispatcher.h"
#include <gitinfo.h>
#define ADD_QUOTES_HELPER(s) #s
@ -151,6 +152,8 @@ void Application::terminate()
// terminate proxy
g_proxy.terminate();
g_asyncDispatcher.reset();
m_terminated = true;
signal(SIGTERM, SIG_DFL);

View file

@ -38,4 +38,4 @@ uint8_t getThreadCount() {
return std::clamp<int_fast8_t>(std::thread::hardware_concurrency() - 1, MIN_THREADS, MAX_THREADS);
}
AsyncDispatcher g_asyncDispatcher{ getThreadCount() };
std::unique_ptr<AsyncDispatcher> g_asyncDispatcher = std::make_unique<AsyncDispatcher>(getThreadCount());

View file

@ -25,7 +25,8 @@
#include <cstddef>
#include <BS_thread_pool.hpp>
#include <memory>
using AsyncDispatcher = decltype(BS::thread_pool{ std::size_t{} });
extern AsyncDispatcher g_asyncDispatcher;
extern std::unique_ptr<AsyncDispatcher> g_asyncDispatcher;

View file

@ -31,7 +31,7 @@ int16_t g_mainThreadId = stdext::getThreadId();
int16_t g_eventThreadId = -1;
void EventDispatcher::init() {
for (size_t i = 0; i < g_asyncDispatcher.get_thread_count(); ++i) {
for (size_t i = 0; i < g_asyncDispatcher->get_thread_count(); ++i) {
m_threads.emplace_back(std::make_unique<ThreadTask>());
}
}

View file

@ -198,7 +198,7 @@ void GraphicalApplication::run()
};
#endif
// THREAD - POOL & MAP
const auto& mapThread = g_asyncDispatcher.submit_task([this] {
const auto& mapThread = g_asyncDispatcher->submit_task([this] {
BS::multi_future<void> tasks;
g_luaThreadId = g_eventThreadId = stdext::getThreadId();
@ -214,7 +214,7 @@ void GraphicalApplication::run()
if (canDrawMap()) {
if (canDrawForeground) {
tasks.emplace_back(g_asyncDispatcher.submit_task([] {
tasks.emplace_back(g_asyncDispatcher->submit_task([] {
AutoStat s(STATS_RENDER, "DrawForegroundUI");
g_ui.render(DrawPoolType::FOREGROUND);
}));
@ -227,7 +227,7 @@ void GraphicalApplication::run()
static constexpr std::array<DrawPoolType, 2> types{ DrawPoolType::LIGHT, DrawPoolType::FOREGROUND_MAP };
for (const auto type : types) {
if (m_drawEvents->canDraw(type)) {
tasks.emplace_back(g_asyncDispatcher.submit_task([this, type] {
tasks.emplace_back(g_asyncDispatcher->submit_task([this, type] {
AutoStat s(STATS_RENDER, type == DrawPoolType::LIGHT ? "DrawLight" : "DrawForegroundMap");
m_drawEvents->draw(type);
}));
@ -382,7 +382,7 @@ void GraphicalApplication::doScreenshot(std::string file)
auto pixels = std::make_shared<std::vector<uint8_t>>(width * height * 4 * sizeof(GLubyte), 0);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels->data());
g_asyncDispatcher.detach_task([resolution, pixels, file] {
g_asyncDispatcher->detach_task([resolution, pixels, file] {
try {
Image image(resolution, 4, pixels->data());
image.flipVertically();

View file

@ -220,6 +220,6 @@ void ModuleManager::enableAutoReload() {
return;
processing.store(true);
g_asyncDispatcher.detach_task(action);
g_asyncDispatcher->detach_task(action);
}, 500);
}

View file

@ -61,7 +61,8 @@ bool ResourceManager::discoverWorkDir(const std::string& existentFile)
g_resources.getBaseDir(),
g_resources.getBaseDir() + "/game_data/",
g_resources.getBaseDir() + "../",
g_resources.getBaseDir() + "../share/" + g_app.getCompactName() + "/" };
g_resources.getBaseDir() + "../share/" + g_app.getCompactName() + "/",
};
bool found = false;
for (const auto& dir : possiblePaths) {
@ -703,21 +704,33 @@ bool ResourceManager::launchCorrect(const std::vector<std::string>& args) { // c
#if (defined(ANDROID) || defined(FREE_VERSION))
return false;
#else
auto fileName2 = m_binaryPath.stem().string();
fileName2 = stdext::split(fileName2, "-")[0];
stdext::tolower(fileName2);
const auto normalizeName = [](std::string name) {
const auto dash = name.find('-');
if (dash != std::string::npos) {
name = name.substr(0, dash);
}
stdext::tolower(name);
return name;
};
auto fileName2 = normalizeName(m_binaryPath.stem().string());
const std::filesystem::path path(m_binaryPath.parent_path());
std::error_code ec;
if (path.empty() || !std::filesystem::exists(path, ec) || ec) {
return false;
}
auto lastWrite = last_write_time(m_binaryPath, ec);
std::filesystem::path binary = m_binaryPath;
for (auto& entry : std::filesystem::directory_iterator(path)) {
for (auto it = std::filesystem::directory_iterator(path, ec);
!ec && it != std::filesystem::directory_iterator();
++it) {
const auto& entry = *it;
if (is_directory(entry.path()))
continue;
auto fileName1 = entry.path().stem().string();
fileName1 = stdext::split(fileName1, "-")[0];
stdext::tolower(fileName1);
auto fileName1 = normalizeName(entry.path().stem().string());
if (fileName1 != fileName2)
continue;
@ -731,13 +744,18 @@ bool ResourceManager::launchCorrect(const std::vector<std::string>& args) { // c
}
}
for (auto& entry : std::filesystem::directory_iterator(path)) { // remove old
if (ec) {
return false;
}
for (auto it = std::filesystem::directory_iterator(path, ec);
!ec && it != std::filesystem::directory_iterator();
++it) { // remove old
const auto& entry = *it;
if (is_directory(entry.path()))
continue;
auto fileName1 = entry.path().stem().string();
fileName1 = stdext::split(fileName1, "-")[0];
stdext::tolower(fileName1);
auto fileName1 = normalizeName(entry.path().stem().string());
if (fileName1 != fileName2)
continue;
@ -749,6 +767,10 @@ bool ResourceManager::launchCorrect(const std::vector<std::string>& args) { // c
}
}
if (ec) {
return false;
}
if (binary == m_binaryPath)
return false;

View file

@ -24,6 +24,7 @@
#include "painter.h"
#include "textureatlas.h"
#include "coordsbuffer.h"
DrawPool* DrawPool::create(const DrawPoolType type)
{

View file

@ -197,7 +197,7 @@ void FrameBuffer::doScreenshot(std::string file, const uint16_t x, const uint16_
internalRelease();
g_asyncDispatcher.detach_task([size, pixels, file] {
g_asyncDispatcher->detach_task([size, pixels, file] {
try {
Image image(size, 4, pixels->data());
image.flipVertically();

View file

@ -27,6 +27,10 @@
#define GL_APICALL
#define EGLAPI
#include <GLES3/gl3.h>
#elif defined(__APPLE__)
#define GLEW_STATIC
#define GLEW_NO_GLU
#include <GL/glew.h>
#else
#ifndef _MSC_VER
#define GLEW_STATIC

View file

@ -29,6 +29,11 @@
#include "framework/platform/staticdata.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <type_traits>
template<typename T>
int push_internal_luavalue(T v);
@ -44,6 +49,32 @@ bool luavalue_cast(int index, int& i);
int push_luavalue(double d);
bool luavalue_cast(int index, double& d);
namespace detail {
constexpr double maxExactIntegerAsDouble = 9007199254740992.0; // 2^53
template<typename T>
inline bool luavalue_cast_integer_from_double(const int index, T& v)
{
static_assert(std::is_integral_v<T>);
double d;
if (!luavalue_cast(index, d))
return false;
if (!std::isfinite(d) || std::trunc(d) != d)
return false;
const double typeMin = static_cast<double>(std::numeric_limits<T>::lowest());
const double typeMax = static_cast<double>(std::numeric_limits<T>::max());
const double exactMin = std::numeric_limits<T>::is_signed ? -maxExactIntegerAsDouble : 0.0;
const double exactMax = maxExactIntegerAsDouble;
if (d < std::max(typeMin, exactMin) || d > std::min(typeMax, exactMax))
return false;
v = static_cast<T>(d);
return true;
}
}
// float
inline int push_luavalue(const float f) { push_luavalue(static_cast<double>(f)); return 1; }
inline bool luavalue_cast(const int index, float& f)
@ -91,8 +122,14 @@ inline bool luavalue_cast(const int index, uint32_t& v)
inline int push_luavalue(const int64_t v) { push_luavalue(static_cast<double>(v)); return 1; }
inline bool luavalue_cast(const int index, int64_t& v)
{
double d;
const bool r = luavalue_cast(index, d); v = d; return r;
return detail::luavalue_cast_integer_from_double(index, v);
}
// uint64
inline int push_luavalue(const uint64_t v) { push_luavalue(static_cast<double>(v)); return 1; }
inline bool luavalue_cast(const int index, uint64_t& v)
{
return detail::luavalue_cast_integer_from_double(index, v);
}
using lua_u64 = std::conditional_t<sizeof(unsigned long) == 8, unsigned long, std::uint64_t>;
@ -100,6 +137,7 @@ using lua_unsigned_long = lua_u64;
static_assert(sizeof(lua_u64) == 8, "lua_u64 must be 64-bit");
template<typename T = unsigned long> requires (!std::is_same_v<T, unsigned long long>)
inline int push_luavalue(const unsigned long v)
{
if constexpr (sizeof(unsigned long) <= sizeof(uint32_t)) {
@ -110,6 +148,7 @@ inline int push_luavalue(const unsigned long v)
return 1;
}
template<typename T = unsigned long> requires (!std::is_same_v<T, unsigned long long>)
inline bool luavalue_cast(const int index, unsigned long& v)
{
if constexpr (sizeof(unsigned long) <= sizeof(uint32_t)) {
@ -119,26 +158,20 @@ inline bool luavalue_cast(const int index, unsigned long& v)
return r;
}
double temp;
const bool r = luavalue_cast(index, temp);
v = static_cast<unsigned long>(temp);
return r;
return detail::luavalue_cast_integer_from_double(index, v);
}
template<typename T = lua_u64, std::enable_if_t<!std::is_same_v<T, unsigned long>, int> = 0>
template<typename T = lua_u64> requires (!std::is_same_v<T, unsigned long>)
inline int push_luavalue(lua_u64 v)
{
push_luavalue(static_cast<double>(v));
return 1;
}
template<typename T = lua_u64, std::enable_if_t<!std::is_same_v<T, unsigned long>, int> = 0>
template<typename T = lua_u64> requires (!std::is_same_v<T, unsigned long> && !std::is_same_v<T, unsigned long long>)
inline bool luavalue_cast(const int idx, lua_u64& v)
{
double d;
const bool r = luavalue_cast(idx, d);
v = static_cast<lua_u64>(d);
return r;
return detail::luavalue_cast_integer_from_double(idx, v);
}
// string

View file

@ -20,6 +20,15 @@
* THE SOFTWARE.
*/
#ifndef CPPHTTPLIB_OPENSSL_SUPPORT
# define CPPHTTPLIB_OPENSSL_SUPPORT
#endif
#ifdef __APPLE__
# undef CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO
# define CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES
#endif
#include <httplib.h>
#include "httplogin.h"
#include <framework/core/asyncdispatcher.h>
#include <framework/core/eventdispatcher.h>
@ -102,17 +111,17 @@ void LoginHttp::httpLogin(const std::string& host, const std::string& path,
const std::string& password, int request_id, bool httpLogin, const std::string& token) {
#ifndef __EMSCRIPTEN__
this->errorMessage.clear();
g_asyncDispatcher.detach_task(
[this, host, path, port, email, password, request_id, token, httpLogin] {
g_asyncDispatcher->detach_task(
[this, host, path, port, email, password, token, request_id, httpLogin] {
if (cancelled.load()) return;
httplib::Result result =
HttpResponse result =
this->loginHttpsJson(host, path, port, email, password, token);
if (httpLogin && (!result || result->status != Success)) {
if (httpLogin && (!result || result.status != Success)) {
if (cancelled.load()) return;
result = loginHttpJson(host, path, port, email, password, token);
}
if (result && result->status == Success && parseJsonResponse(result->body)) {
if (result && result.status == Success && parseJsonResponse(result.body)) {
g_dispatcher.addEvent([this, request_id] {
if (cancelled.load()) return;
g_lua.callGlobalField("EnterGame", "loginSuccess", request_id,
@ -123,12 +132,12 @@ void LoginHttp::httpLogin(const std::string& host, const std::string& path,
int status = 0;
std::string msg = "";
if (result) {
status = result->status;
status = result.status;
if (!this->errorMessage.empty()) {
msg = this->errorMessage;
}
try {
const auto body = json::parse(result->body);
const auto body = json::parse(result.body);
if (msg.empty()) {
msg = body.value("errorMessage", "");
}
@ -138,8 +147,8 @@ void LoginHttp::httpLogin(const std::string& host, const std::string& path,
if (msg.empty()) {
if (status != Success) {
msg = "HTTP " + std::to_string(status);
if (!result->reason.empty()) {
msg += " - " + result->reason;
if (!result.reason.empty()) {
msg += " - " + result.reason;
} else {
msg += " - Unknown status";
}
@ -167,8 +176,9 @@ void LoginHttp::httpLogin(const std::string& host, const std::string& path,
});
#else
this->errorMessage.clear();
g_asyncDispatcher.detach_task(
[this, host, path, port, email, password, request_id, token, httpLogin] {
g_asyncDispatcher->detach_task(
[this, host, path, port, email, password, token, request_id, httpLogin] {
if (cancelled.load()) return;
emscripten_fetch_attr_t attr;
emscripten_fetch_attr_init(&attr);
strcpy(attr.requestMethod, "POST");
@ -245,12 +255,12 @@ void LoginHttp::httpLogin(const std::string& host, const std::string& path,
#endif
}
httplib::Result LoginHttp::loginHttpsJson(const std::string& host,
const std::string& path,
const uint16_t port,
const std::string& email,
const std::string& password,
const std::string& token) {
LoginHttp::HttpResponse LoginHttp::loginHttpsJson(const std::string& host,
const std::string& path,
const uint16_t port,
const std::string& email,
const std::string& password,
const std::string& token) {
httplib::SSLClient client(host, port);
client.set_logger(
@ -291,15 +301,18 @@ httplib::Result LoginHttp::loginHttpsJson(const std::string& host,
<< std::endl;
}
return response;
if (!response)
return {};
return { true, response->status, response->reason, response->body };
}
httplib::Result LoginHttp::loginHttpJson(const std::string& host,
const std::string& path,
const uint16_t port,
const std::string& email,
const std::string& password,
const std::string& token) {
LoginHttp::HttpResponse LoginHttp::loginHttpJson(const std::string& host,
const std::string& path,
const uint16_t port,
const std::string& email,
const std::string& password,
const std::string& token) {
httplib::Client client(host, port);
client.set_logger(
[this](const auto& req, const auto& res) { LoginHttp::Logger(req, res); });
@ -332,11 +345,10 @@ httplib::Result LoginHttp::loginHttpJson(const std::string& host,
std::cout << "HTTP status: " << to_string(response.error())
<< std::endl;
}
if (response && response->status == Success && !parseJsonResponse(response->body)) {
return response;
}
if (!response)
return {};
return response;
return { true, response->status, response->reason, response->body };
}
bool LoginHttp::parseJsonResponse(const std::string& body) {
@ -373,4 +385,4 @@ bool LoginHttp::parseJsonResponse(const std::string& body) {
this->worlds = to_string(playdata["worlds"]);
return true;
}
}

View file

@ -22,11 +22,11 @@
#pragma once
#ifndef CPPHTTPLIB_OPENSSL_SUPPORT
# define CPPHTTPLIB_OPENSSL_SUPPORT
#endif
#include <atomic>
#include <cstdint>
#include <string>
#include <framework/luaengine/luaobject.h>
#include <httplib.h>
class LoginHttp final : public LuaObject
{
@ -51,23 +51,32 @@ public:
uint16_t port, const std::string& email,
const std::string& password, int request_id, bool httpLogin, const std::string& token);
httplib::Result loginHttpsJson(const std::string& host,
const std::string& path, uint16_t port,
const std::string& email,
const std::string& password,
const std::string& token);
httplib::Result loginHttpJson(const std::string& host,
const std::string& path, uint16_t port,
const std::string& email,
const std::string& password,
const std::string& token);
void cancel();
enum Result : int { Success = 200, Error = -1 };
private:
struct HttpResponse {
bool connected{ false };
int status{ Error };
std::string reason;
std::string body;
explicit operator bool() const { return connected; }
};
HttpResponse loginHttpsJson(const std::string& host,
const std::string& path, uint16_t port,
const std::string& email,
const std::string& password,
const std::string& token);
HttpResponse loginHttpJson(const std::string& host,
const std::string& path, uint16_t port,
const std::string& email,
const std::string& password,
const std::string& token);
std::string characters;
std::string worlds;
std::string session;

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2010-2025 OTClient <https://github.com/edubart/otclient>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#pragma once
#ifdef __OBJC__
#define Point MacPoint
#define Size MacSize
#define Rect MacRect
#ifndef GL_SILENCE_DEPRECATION
#define GL_SILENCE_DEPRECATION
#endif
#import <Cocoa/Cocoa.h>
#import <AppKit/AppKit.h>
#undef Point
#undef Size
#undef Rect
#ifdef __cplusplus
class CocoaWindow;
#else
typedef void CocoaWindow;
#endif
@interface OTOpenGLView : NSOpenGLView
@property (nonatomic, assign) CocoaWindow* platformWindow;
@property (nonatomic) BOOL acceptsInput;
@property (nonatomic) CGFloat scrollAccumX;
@property (nonatomic) CGFloat scrollAccumY;
@end
@interface OTWindowDelegate : NSObject <NSWindowDelegate>
@property (nonatomic, assign) CocoaWindow* platformWindow;
@end
#endif

View file

@ -0,0 +1,271 @@
/*
* Copyright (c) 2010-2025 OTClient <https://github.com/edubart/otclient>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifdef __APPLE__
#include "cocoawindow.h"
#include "cocoaview.h"
#include <string>
#define Point MacPoint
#define Size MacSize
#define Rect MacRect
#import <Carbon/Carbon.h>
#undef Point
#undef Size
#undef Rect
@interface OTOpenGLView ()
- (void)dispatchMouseMove:(NSEvent*)event;
- (void)dispatchMouseButton:(Fw::MouseButton)button pressed:(bool)pressed event:(NSEvent*)event;
@end
@implementation OTOpenGLView
- (instancetype)initWithFrame:(NSRect)frameRect pixelFormat:(NSOpenGLPixelFormat*)format {
self = [super initWithFrame:frameRect pixelFormat:format];
if (self) {
_acceptsInput = YES;
NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited |
NSTrackingMouseMoved |
NSTrackingActiveAlways |
NSTrackingInVisibleRect;
NSTrackingArea* trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect
options:options
owner:self
userInfo:nil];
[self addTrackingArea:trackingArea];
}
return self;
}
- (BOOL)acceptsFirstResponder {
return _acceptsInput;
}
- (BOOL)canBecomeKeyView {
return _acceptsInput;
}
- (void)keyDown:(NSEvent*)event {
if (!_platformWindow) return;
// Skip OS-level key repeats - game handles repeats via fireKeysPress()
if ([event isARepeat]) {
if ([event characters] && [[event characters] length] > 0) {
std::string chars = [[event characters] UTF8String];
Fw::Key key = _platformWindow->translateKeyCode([event keyCode]);
bool isSpecial = _platformWindow->isSpecialKey(key);
unsigned int modifiers = [event modifierFlags];
if (!isSpecial && !(modifiers & (NSEventModifierFlagCommand | NSEventModifierFlagControl))) {
_platformWindow->handleTextInput(chars);
}
}
return;
}
unsigned short keyCode = [event keyCode];
unsigned int modifiers = [event modifierFlags];
if ((modifiers & NSEventModifierFlagCommand) && keyCode == kVK_ANSI_Q) {
_platformWindow->handleClose();
return;
}
std::string chars;
if ([event characters] && [[event characters] length] > 0) {
chars = [[event characters] UTF8String];
}
_platformWindow->handleKeyDown(keyCode, modifiers, chars);
Fw::Key key = _platformWindow->translateKeyCode(keyCode);
bool isSpecial = _platformWindow->isSpecialKey(key);
if (chars.length() > 0 && !isSpecial && !(modifiers & (NSEventModifierFlagCommand | NSEventModifierFlagControl))) {
_platformWindow->handleTextInput(chars);
}
}
- (void)keyUp:(NSEvent*)event {
if (!_platformWindow) return;
_platformWindow->handleKeyUp([event keyCode], [event modifierFlags]);
}
- (void)flagsChanged:(NSEvent*)event {
if (!_platformWindow) return;
_platformWindow->handleFlagsChanged([event modifierFlags]);
}
- (Point)convertMousePosition:(NSEvent*)event {
NSPoint loc = [self convertPoint:[event locationInWindow] fromView:nil];
NSRect bounds = [self bounds];
return Point(static_cast<int>(loc.x), static_cast<int>(bounds.size.height - loc.y));
}
- (void)dispatchMouseMove:(NSEvent*)event {
if (!_platformWindow) return;
_platformWindow->handleMouseMove([self convertMousePosition:event]);
}
- (void)dispatchMouseButton:(Fw::MouseButton)button pressed:(bool)pressed event:(NSEvent*)event {
if (!_platformWindow) return;
_platformWindow->handleMouseButton(button, pressed, [self convertMousePosition:event]);
}
- (void)mouseDown:(NSEvent*)event {
[self dispatchMouseButton:Fw::MouseLeftButton pressed:true event:event];
}
- (void)mouseUp:(NSEvent*)event {
[self dispatchMouseButton:Fw::MouseLeftButton pressed:false event:event];
}
- (void)rightMouseDown:(NSEvent*)event {
[self dispatchMouseButton:Fw::MouseRightButton pressed:true event:event];
}
- (void)rightMouseUp:(NSEvent*)event {
[self dispatchMouseButton:Fw::MouseRightButton pressed:false event:event];
}
- (void)otherMouseDown:(NSEvent*)event {
if ([event buttonNumber] == 2) {
[self dispatchMouseButton:Fw::MouseMidButton pressed:true event:event];
}
}
- (void)otherMouseUp:(NSEvent*)event {
if ([event buttonNumber] == 2) {
[self dispatchMouseButton:Fw::MouseMidButton pressed:false event:event];
}
}
- (void)mouseMoved:(NSEvent*)event {
[self dispatchMouseMove:event];
}
- (void)mouseDragged:(NSEvent*)event {
[self dispatchMouseMove:event];
}
- (void)rightMouseDragged:(NSEvent*)event {
[self dispatchMouseMove:event];
}
- (void)otherMouseDragged:(NSEvent*)event {
[self dispatchMouseMove:event];
}
- (void)scrollWheel:(NSEvent*)event {
if (!_platformWindow) return;
CGFloat deltaX = [event scrollingDeltaX];
CGFloat deltaY = [event scrollingDeltaY];
if ([event hasPreciseScrollingDeltas]) {
_scrollAccumX += deltaX;
_scrollAccumY += deltaY;
int intDX = static_cast<int>(_scrollAccumX);
int intDY = static_cast<int>(_scrollAccumY);
if (intDX != 0 || intDY != 0) {
_platformWindow->handleMouseScroll(intDX, intDY);
_scrollAccumX -= intDX;
_scrollAccumY -= intDY;
}
} else {
_platformWindow->handleMouseScroll(static_cast<int>(deltaX), static_cast<int>(deltaY));
}
}
- (void)mouseEntered:(NSEvent*)event {
(void)event;
}
- (void)mouseExited:(NSEvent*)event {
(void)event;
}
@end
@implementation OTWindowDelegate
- (void)windowDidResize:(NSNotification*)notification {
if (!_platformWindow) return;
NSWindow* window = [notification object];
NSRect contentRect = [[window contentView] frame];
_platformWindow->handleResize(static_cast<int>(contentRect.size.width),
static_cast<int>(contentRect.size.height));
}
- (void)windowDidMove:(NSNotification*)notification {
if (!_platformWindow) return;
NSWindow* window = [notification object];
NSRect frame = [window frame];
NSScreen* screen = [window screen];
if (screen) {
NSRect screenFrame = [screen frame];
int y = static_cast<int>(screenFrame.origin.y + screenFrame.size.height - frame.origin.y - frame.size.height);
_platformWindow->handleMove(static_cast<int>(frame.origin.x), y);
}
}
- (void)windowDidBecomeKey:(NSNotification*)notification {
(void)notification;
if (!_platformWindow) return;
_platformWindow->handleFocusChange(true);
}
- (void)windowDidResignKey:(NSNotification*)notification {
(void)notification;
if (!_platformWindow) return;
_platformWindow->handleFocusChange(false);
}
- (BOOL)windowShouldClose:(NSWindow*)sender {
(void)sender;
if (_platformWindow) {
_platformWindow->handleClose();
}
return NO;
}
- (void)windowWillEnterFullScreen:(NSNotification*)notification {
(void)notification;
}
- (void)windowDidEnterFullScreen:(NSNotification*)notification {
(void)notification;
}
- (void)windowWillExitFullScreen:(NSNotification*)notification {
(void)notification;
}
- (void)windowDidExitFullScreen:(NSNotification*)notification {
(void)notification;
}
@end
#endif

View file

@ -0,0 +1,296 @@
/*
* Copyright (c) 2010-2025 OTClient <https://github.com/edubart/otclient>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#pragma once
#include "platformwindow.h"
#include <array>
#ifdef __OBJC__
@class NSWindow;
@class NSOpenGLView;
@class NSOpenGLContext;
@class NSCursor;
@class OTOpenGLView;
@class OTWindowDelegate;
#else
typedef void NSWindow;
typedef void NSOpenGLView;
typedef void NSOpenGLContext;
typedef void NSCursor;
typedef void OTOpenGLView;
typedef void OTWindowDelegate;
#endif
class CocoaWindow : public PlatformWindow
{
public:
/**
* Creates a new Cocoa window instance.
* Initializes member variables and key mappings for macOS input handling.
*/
CocoaWindow();
/**
* Destroys the Cocoa window and releases all associated resources.
*/
~CocoaWindow();
/**
* Initializes the window and OpenGL context.
* Creates the native NSWindow, OpenGL view, and event handlers.
*/
void init() override;
/**
* Terminates the window and releases all resources.
* Destroys the NSWindow, OpenGL context, and event handlers.
*/
void terminate() override;
/**
* Moves the window to the specified position.
* @param pos The new top-left position of the window.
*/
void move(const Point& pos) override;
/**
* Resizes the window to the specified size.
* @param size The new size of the window.
*/
void resize(const Size& size) override;
/** Makes the window visible on screen. */
void show() override;
/** Hides the window from screen. */
void hide() override;
/** Maximizes the window to fill the screen. */
void maximize() override;
/**
* Processes pending window events.
* Pumps the event loop and dispatches pending events.
*/
void poll() override;
/** Swaps the OpenGL front and back buffers. */
void swapBuffers() override;
/** Shows the mouse cursor. */
void showMouse() override;
/** Hides the mouse cursor. */
void hideMouse() override;
/**
* Sets the mouse cursor to a predefined style.
* @param cursorId The cursor style identifier.
*/
void setMouseCursor(int cursorId) override;
/** Restores the default mouse cursor. */
void restoreMouseCursor() override;
/**
* Sets the window title.
* @param title The title string to display.
*/
void setTitle(std::string_view title) override;
/**
* Sets the minimum window size.
* @param minimumSize The minimum allowed window dimensions.
*/
void setMinimumSize(const Size& minimumSize) override;
/**
* Toggles fullscreen mode.
* @param fullscreen True to enter fullscreen, false to exit.
*/
void setFullscreen(bool fullscreen) override;
/**
* Enables or disables vertical sync.
* @param enable True to enable v-sync, false to disable.
*/
void setVerticalSync(bool enable) override;
/**
* Sets the window icon from an image file.
* @param file Path to the icon image file.
*/
void setIcon(const std::string& file) override;
/**
* Copies text to the system clipboard.
* @param text The text to copy to the clipboard.
*/
void setClipboardText(std::string_view text) override;
/**
* Gets the primary display size.
* @return The dimensions of the main display in pixels.
*/
Size getDisplaySize() override;
/**
* Retrieves text from the system clipboard.
* @return The clipboard text content, or empty string if unavailable.
*/
std::string getClipboardText() override;
/**
* Returns a string identifying the platform type.
* @return Platform identifier string.
*/
std::string getPlatformType() override;
/**
* Handles key down events from the native event loop.
* @param keyCode The macOS virtual key code.
* @param modifiers The current modifier flags.
* @param characters The character string for this key event.
*/
void handleKeyDown(unsigned short keyCode, unsigned int modifiers, const std::string& characters);
/**
* Handles key up events from the native event loop.
* @param keyCode The macOS virtual key code.
* @param modifiers The current modifier flags.
*/
void handleKeyUp(unsigned short keyCode, unsigned int modifiers);
/** Handles changes to modifier key states. */
void handleFlagsChanged(unsigned int modifiers);
/**
* Handles mouse button events.
* @param button The mouse button identifier.
* @param pressed True if button was pressed, false if released.
* @param position The mouse position in window coordinates.
*/
void handleMouseButton(Fw::MouseButton button, bool pressed, const Point& position);
/**
* Handles mouse movement events.
* @param position The new mouse position in window coordinates.
*/
void handleMouseMove(const Point& position);
/**
* Handles mouse scroll wheel events.
* @param deltaX Horizontal scroll delta.
* @param deltaY Vertical scroll delta.
*/
void handleMouseScroll(int deltaX, int deltaY);
/**
* Handles window resize events.
* @param width The new window width.
* @param height The new window height.
*/
void handleResize(int width, int height);
/**
* Handles window move events.
* @param x The new x position.
* @param y The new y position.
*/
void handleMove(int x, int y);
/** Handles window close requests. */
void handleClose();
/**
* Handles focus change events.
* @param focused True if window gained focus, false if lost.
*/
void handleFocusChange(bool focused);
/**
* Handles text input events for character composition.
* @param text The input text string.
*/
void handleTextInput(const std::string& text);
/**
* Translates a macOS virtual key code to the framework key enum.
* @param keyCode The macOS virtual key code.
* @return The corresponding framework key identifier.
*/
Fw::Key translateKeyCode(unsigned short keyCode);
/**
* Checks if a key is a special function key.
* @param key The key to check.
* @return True if the key is a special key (function keys, etc.).
*/
bool isSpecialKey(Fw::Key key);
protected:
/**
* Loads a custom mouse cursor from an image.
* @param image The image to create the cursor from.
* @param hotSpot The hotspot point for the cursor.
* @return The cursor ID on success, -1 on failure.
*/
int internalLoadMouseCursor(const ImagePtr& image, const Point& hotSpot) override;
private:
/**
* Creates the native NSWindow and initializes all UI components.
*/
void internalCreateWindow();
/**
* Creates the OpenGL context and configures it for rendering.
*/
void internalCreateGLContext();
/**
* Initializes the key code mapping table for macOS key codes.
*/
void internalInitKeyMap();
/**
* Updates the current modifier key state.
* @param modifiers The new modifier flags.
*/
void updateModifiers(unsigned int modifiers);
NSWindow* m_window;
OTOpenGLView* m_glView;
OTWindowDelegate* m_delegate;
NSOpenGLContext* m_glContext;
std::vector<NSCursor*> m_cursors;
NSCursor* m_currentCursor;
NSCursor* m_defaultCursor;
bool m_cursorHidden;
bool m_cursorInWindow;
unsigned int m_lastModifiers;
std::array<bool, Fw::KeyLast> m_commandKeyDown{};
};

View file

@ -0,0 +1,824 @@
/*
* Copyright (c) 2010-2025 OTClient <https://github.com/edubart/otclient>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifdef __APPLE__
#define GLEW_STATIC
#define GLEW_NO_GLU
#include <GL/glew.h>
#define Point MacPoint
#define Size MacSize
#define Rect MacRect
#define GL_SILENCE_DEPRECATION
#import <Cocoa/Cocoa.h>
#import <AppKit/AppKit.h>
#import <Carbon/Carbon.h>
#import <QuartzCore/CVDisplayLink.h>
#undef Point
#undef Size
#undef Rect
#include "cocoawindow.h"
#include "cocoaview.h"
#include <framework/core/eventdispatcher.h>
#include <framework/graphics/image.h>
CocoaWindow::CocoaWindow()
{
m_window = nil;
m_glView = nil;
m_delegate = nil;
m_glContext = nil;
m_currentCursor = nil;
m_defaultCursor = nil;
m_cursorHidden = false;
m_cursorInWindow = false;
m_lastModifiers = 0;
m_minimumSize = Size(600, 480);
m_size = Size(600, 480);
internalInitKeyMap();
}
CocoaWindow::~CocoaWindow()
{
terminate();
}
void CocoaWindow::internalInitKeyMap()
{
m_keyMap[kVK_Escape] = Fw::KeyEscape;
m_keyMap[kVK_Tab] = Fw::KeyTab;
m_keyMap[kVK_Return] = Fw::KeyEnter;
m_keyMap[kVK_Delete] = Fw::KeyBackspace;
m_keyMap[kVK_Space] = Fw::KeySpace;
m_keyMap[kVK_PageUp] = Fw::KeyPageUp;
m_keyMap[kVK_PageDown] = Fw::KeyPageDown;
m_keyMap[kVK_Home] = Fw::KeyHome;
m_keyMap[kVK_End] = Fw::KeyEnd;
m_keyMap[kVK_ForwardDelete] = Fw::KeyDelete;
m_keyMap[kVK_UpArrow] = Fw::KeyUp;
m_keyMap[kVK_DownArrow] = Fw::KeyDown;
m_keyMap[kVK_LeftArrow] = Fw::KeyLeft;
m_keyMap[kVK_RightArrow] = Fw::KeyRight;
m_keyMap[kVK_CapsLock] = Fw::KeyCapsLock;
m_keyMap[kVK_Control] = Fw::KeyCtrl;
m_keyMap[kVK_RightControl] = Fw::KeyCtrl;
m_keyMap[kVK_Shift] = Fw::KeyShift;
m_keyMap[kVK_RightShift] = Fw::KeyShift;
m_keyMap[kVK_Option] = Fw::KeyAlt;
m_keyMap[kVK_RightOption] = Fw::KeyAlt;
m_keyMap[kVK_Command] = Fw::KeyMeta;
m_keyMap[kVK_RightCommand] = Fw::KeyMeta;
m_keyMap[kVK_ANSI_A] = Fw::KeyA;
m_keyMap[kVK_ANSI_B] = Fw::KeyB;
m_keyMap[kVK_ANSI_C] = Fw::KeyC;
m_keyMap[kVK_ANSI_D] = Fw::KeyD;
m_keyMap[kVK_ANSI_E] = Fw::KeyE;
m_keyMap[kVK_ANSI_F] = Fw::KeyF;
m_keyMap[kVK_ANSI_G] = Fw::KeyG;
m_keyMap[kVK_ANSI_H] = Fw::KeyH;
m_keyMap[kVK_ANSI_I] = Fw::KeyI;
m_keyMap[kVK_ANSI_J] = Fw::KeyJ;
m_keyMap[kVK_ANSI_K] = Fw::KeyK;
m_keyMap[kVK_ANSI_L] = Fw::KeyL;
m_keyMap[kVK_ANSI_M] = Fw::KeyM;
m_keyMap[kVK_ANSI_N] = Fw::KeyN;
m_keyMap[kVK_ANSI_O] = Fw::KeyO;
m_keyMap[kVK_ANSI_P] = Fw::KeyP;
m_keyMap[kVK_ANSI_Q] = Fw::KeyQ;
m_keyMap[kVK_ANSI_R] = Fw::KeyR;
m_keyMap[kVK_ANSI_S] = Fw::KeyS;
m_keyMap[kVK_ANSI_T] = Fw::KeyT;
m_keyMap[kVK_ANSI_U] = Fw::KeyU;
m_keyMap[kVK_ANSI_V] = Fw::KeyV;
m_keyMap[kVK_ANSI_W] = Fw::KeyW;
m_keyMap[kVK_ANSI_X] = Fw::KeyX;
m_keyMap[kVK_ANSI_Y] = Fw::KeyY;
m_keyMap[kVK_ANSI_Z] = Fw::KeyZ;
m_keyMap[kVK_ANSI_0] = Fw::Key0;
m_keyMap[kVK_ANSI_1] = Fw::Key1;
m_keyMap[kVK_ANSI_2] = Fw::Key2;
m_keyMap[kVK_ANSI_3] = Fw::Key3;
m_keyMap[kVK_ANSI_4] = Fw::Key4;
m_keyMap[kVK_ANSI_5] = Fw::Key5;
m_keyMap[kVK_ANSI_6] = Fw::Key6;
m_keyMap[kVK_ANSI_7] = Fw::Key7;
m_keyMap[kVK_ANSI_8] = Fw::Key8;
m_keyMap[kVK_ANSI_9] = Fw::Key9;
m_keyMap[kVK_ANSI_Minus] = Fw::KeyMinus;
m_keyMap[kVK_ANSI_Equal] = Fw::KeyEqual;
m_keyMap[kVK_ANSI_LeftBracket] = Fw::KeyLeftBracket;
m_keyMap[kVK_ANSI_RightBracket] = Fw::KeyRightBracket;
m_keyMap[kVK_ANSI_Backslash] = Fw::KeyBackslash;
m_keyMap[kVK_ANSI_Semicolon] = Fw::KeySemicolon;
m_keyMap[kVK_ANSI_Quote] = Fw::KeyApostrophe;
m_keyMap[kVK_ANSI_Comma] = Fw::KeyComma;
m_keyMap[kVK_ANSI_Period] = Fw::KeyPeriod;
m_keyMap[kVK_ANSI_Slash] = Fw::KeySlash;
m_keyMap[kVK_ANSI_Grave] = Fw::KeyGrave;
m_keyMap[kVK_F1] = Fw::KeyF1;
m_keyMap[kVK_F2] = Fw::KeyF2;
m_keyMap[kVK_F3] = Fw::KeyF3;
m_keyMap[kVK_F4] = Fw::KeyF4;
m_keyMap[kVK_F5] = Fw::KeyF5;
m_keyMap[kVK_F6] = Fw::KeyF6;
m_keyMap[kVK_F7] = Fw::KeyF7;
m_keyMap[kVK_F8] = Fw::KeyF8;
m_keyMap[kVK_F9] = Fw::KeyF9;
m_keyMap[kVK_F10] = Fw::KeyF10;
m_keyMap[kVK_F11] = Fw::KeyF11;
m_keyMap[kVK_F12] = Fw::KeyF12;
m_keyMap[kVK_ANSI_Keypad0] = Fw::KeyNumpad0;
m_keyMap[kVK_ANSI_Keypad1] = Fw::KeyNumpad1;
m_keyMap[kVK_ANSI_Keypad2] = Fw::KeyNumpad2;
m_keyMap[kVK_ANSI_Keypad3] = Fw::KeyNumpad3;
m_keyMap[kVK_ANSI_Keypad4] = Fw::KeyNumpad4;
m_keyMap[kVK_ANSI_Keypad5] = Fw::KeyNumpad5;
m_keyMap[kVK_ANSI_Keypad6] = Fw::KeyNumpad6;
m_keyMap[kVK_ANSI_Keypad7] = Fw::KeyNumpad7;
m_keyMap[kVK_ANSI_Keypad8] = Fw::KeyNumpad8;
m_keyMap[kVK_ANSI_Keypad9] = Fw::KeyNumpad9;
m_keyMap[kVK_ANSI_KeypadEnter] = Fw::KeyEnter;
}
Fw::Key CocoaWindow::translateKeyCode(unsigned short keyCode)
{
auto it = m_keyMap.find(keyCode);
if (it != m_keyMap.end())
return it->second;
return Fw::KeyUnknown;
}
bool CocoaWindow::isSpecialKey(Fw::Key key)
{
return key == Fw::KeyTab || key == Fw::KeyEnter || key == Fw::KeyEscape ||
key == Fw::KeyBackspace || key == Fw::KeyDelete ||
key == Fw::KeyUp || key == Fw::KeyDown || key == Fw::KeyLeft || key == Fw::KeyRight ||
key == Fw::KeyHome || key == Fw::KeyEnd || key == Fw::KeyPageUp || key == Fw::KeyPageDown ||
(key >= Fw::KeyF1 && key <= Fw::KeyF12);
}
void CocoaWindow::init()
{
@autoreleasepool {
[NSApplication sharedApplication];
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp finishLaunching];
internalCreateWindow();
internalCreateGLContext();
glewExperimental = GL_TRUE;
m_defaultCursor = [NSCursor arrowCursor];
m_currentCursor = m_defaultCursor;
m_created = true;
}
}
void CocoaWindow::internalCreateWindow()
{
NSRect frame = NSMakeRect(0, 0, m_size.width(), m_size.height());
NSUInteger styleMask = NSWindowStyleMaskTitled |
NSWindowStyleMaskClosable |
NSWindowStyleMaskMiniaturizable |
NSWindowStyleMaskResizable;
m_window = [[NSWindow alloc] initWithContentRect:frame
styleMask:styleMask
backing:NSBackingStoreBuffered
defer:NO];
[m_window setTitle:@"OTClient"];
[m_window center];
[m_window setMinSize:NSMakeSize(m_minimumSize.width(), m_minimumSize.height())];
m_delegate = [[OTWindowDelegate alloc] init];
[m_delegate setPlatformWindow:this];
[m_window setDelegate:m_delegate];
NSRect contentRect = [[m_window contentView] frame];
m_size = Size(static_cast<int>(contentRect.size.width),
static_cast<int>(contentRect.size.height));
NSRect windowFrame = [m_window frame];
NSScreen* screen = [m_window screen];
if (screen) {
NSRect screenFrame = [screen frame];
m_position = Point(static_cast<int>(windowFrame.origin.x),
static_cast<int>(screenFrame.size.height - windowFrame.origin.y - windowFrame.size.height));
}
}
void CocoaWindow::internalCreateGLContext()
{
NSOpenGLPixelFormatAttribute attrs[] = {
NSOpenGLPFADoubleBuffer,
NSOpenGLPFADepthSize, 24,
NSOpenGLPFAStencilSize, 8,
NSOpenGLPFAColorSize, 32,
NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersionLegacy,
NSOpenGLPFAAccelerated,
NSOpenGLPFANoRecovery,
0
};
NSOpenGLPixelFormat* pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs];
if (!pixelFormat) {
g_logger.fatal("Failed to create OpenGL pixel format");
return;
}
int width = m_size.width();
int height = m_size.height();
NSRect viewFrame = NSMakeRect(0, 0, width, height);
g_logger.info("Creating GL view with frame: {}x{}", width, height);
m_glView = [[OTOpenGLView alloc] initWithFrame:viewFrame pixelFormat:pixelFormat];
[m_glView setPlatformWindow:this];
[m_glView setWantsBestResolutionOpenGLSurface:NO];
[m_glView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
m_glContext = [m_glView openGLContext];
[m_glContext makeCurrentContext];
[m_window setContentSize:NSMakeSize(width, height)];
[m_window setContentView:m_glView];
[m_window makeFirstResponder:m_glView];
GLint swapInterval = 1;
[m_glContext setValues:&swapInterval forParameter:NSOpenGLContextParameterSwapInterval];
m_vsync = true;
NSRect newFrame = [[m_window contentView] frame];
g_logger.info("GL context created, content view frame: {}x{}", (int)newFrame.size.width, (int)newFrame.size.height);
}
void CocoaWindow::terminate()
{
@autoreleasepool {
if (m_glContext) {
[NSOpenGLContext clearCurrentContext];
m_glContext = nil;
}
if (m_glView) {
[m_glView setPlatformWindow:nullptr];
m_glView = nil;
}
if (m_delegate) {
[m_delegate setPlatformWindow:nullptr];
m_delegate = nil;
}
if (m_window) {
[m_window close];
m_window = nil;
}
m_cursors.clear();
m_created = false;
}
}
void CocoaWindow::move(const Point& pos)
{
@autoreleasepool {
if (!m_window) return;
NSScreen* screen = [m_window screen];
if (!screen) screen = [NSScreen mainScreen];
NSRect screenFrame = [screen frame];
NSRect frame = [m_window frame];
frame.origin.x = pos.x;
frame.origin.y = screenFrame.size.height - pos.y - frame.size.height;
[m_window setFrame:frame display:YES];
}
}
void CocoaWindow::resize(const Size& size)
{
@autoreleasepool {
if (!m_window) return;
NSRect frame = [m_window frame];
NSRect contentRect = [m_window contentRectForFrameRect:frame];
CGFloat titleBarHeight = frame.size.height - contentRect.size.height;
frame.size.width = size.width();
frame.size.height = size.height() + titleBarHeight;
[m_window setFrame:frame display:YES];
}
}
void CocoaWindow::show()
{
@autoreleasepool {
if (!m_window) {
g_logger.info("CocoaWindow::show() - m_window is nil!");
return;
}
g_logger.info("CocoaWindow::show() - making window visible");
[m_window setIsVisible:YES];
[m_window makeKeyAndOrderFront:nil];
[NSApp activateIgnoringOtherApps:YES];
[m_glView setNeedsDisplay:YES];
[NSApp updateWindows];
NSEvent* event;
while ((event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:[NSDate distantPast]
inMode:NSDefaultRunLoopMode
dequeue:YES])) {
[NSApp sendEvent:event];
}
m_visible = true;
g_logger.info("CocoaWindow::show() - done, visible={}", m_visible);
}
}
void CocoaWindow::hide()
{
@autoreleasepool {
if (!m_window) return;
[m_window orderOut:nil];
m_visible = false;
}
}
void CocoaWindow::maximize()
{
@autoreleasepool {
if (!m_window) return;
if (!m_maximized) {
updateUnmaximizedCoords();
[m_window zoom:nil];
m_maximized = true;
}
}
}
void CocoaWindow::poll()
{
@autoreleasepool {
fireKeysPress();
NSEvent* event;
while ((event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:nil
inMode:NSDefaultRunLoopMode
dequeue:YES])) {
[NSApp sendEvent:event];
}
}
}
void CocoaWindow::swapBuffers()
{
@autoreleasepool {
if (m_glContext) {
[m_glContext flushBuffer];
}
}
}
void CocoaWindow::showMouse()
{
@autoreleasepool {
if (m_cursorHidden) {
[NSCursor unhide];
m_cursorHidden = false;
}
}
}
void CocoaWindow::hideMouse()
{
@autoreleasepool {
if (!m_cursorHidden) {
[NSCursor hide];
m_cursorHidden = true;
}
}
}
void CocoaWindow::setMouseCursor(int cursorId)
{
@autoreleasepool {
if (cursorId >= 0 && cursorId < static_cast<int>(m_cursors.size())) {
m_currentCursor = m_cursors[cursorId];
[m_currentCursor set];
}
}
}
void CocoaWindow::restoreMouseCursor()
{
@autoreleasepool {
m_currentCursor = m_defaultCursor;
[m_currentCursor set];
}
}
void CocoaWindow::setTitle(std::string_view title)
{
@autoreleasepool {
if (!m_window) return;
NSString* nsTitle = [[NSString alloc] initWithBytes:title.data()
length:title.size()
encoding:NSUTF8StringEncoding];
[m_window setTitle:nsTitle];
}
}
void CocoaWindow::setMinimumSize(const Size& minimumSize)
{
@autoreleasepool {
m_minimumSize = minimumSize;
if (m_window) {
[m_window setMinSize:NSMakeSize(minimumSize.width(), minimumSize.height())];
}
}
}
void CocoaWindow::setFullscreen(bool fullscreen)
{
@autoreleasepool {
if (!m_window) return;
if (fullscreen != m_fullscreen) {
if (fullscreen) {
updateUnmaximizedCoords();
}
[m_window toggleFullScreen:nil];
m_fullscreen = fullscreen;
}
}
}
void CocoaWindow::setVerticalSync(bool enable)
{
@autoreleasepool {
if (m_glContext) {
GLint swapInterval = enable ? 1 : 0;
[m_glContext setValues:&swapInterval forParameter:NSOpenGLContextParameterSwapInterval];
m_vsync = enable;
}
}
}
void CocoaWindow::setIcon(const std::string& file)
{
@autoreleasepool {
auto image = Image::load(file);
if (!image) {
g_logger.traceError("unable to load icon {}", file);
return;
}
int width = image->getWidth();
int height = image->getHeight();
const uint8_t* pixels = image->getPixelData();
NSBitmapImageRep* rep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:nullptr
pixelsWide:width
pixelsHigh:height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bytesPerRow:width * 4
bitsPerPixel:32];
memcpy([rep bitmapData], pixels, width * height * 4);
CGFloat scale = [[NSScreen mainScreen] backingScaleFactor];
NSSize sizeInPoints = NSMakeSize(width / scale, height / scale);
[rep setSize:sizeInPoints];
NSImage* nsImage = [[NSImage alloc] initWithSize:sizeInPoints];
[nsImage addRepresentation:rep];
[NSApp setApplicationIconImage:nsImage];
}
}
void CocoaWindow::setClipboardText(std::string_view text)
{
@autoreleasepool {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
[pasteboard clearContents];
NSString* nsText = [[NSString alloc] initWithBytes:text.data()
length:text.size()
encoding:NSUTF8StringEncoding];
[pasteboard setString:nsText forType:NSPasteboardTypeString];
}
}
Size CocoaWindow::getDisplaySize()
{
@autoreleasepool {
NSScreen* screen = [NSScreen mainScreen];
NSRect frame = [screen frame];
return Size(static_cast<int>(frame.size.width), static_cast<int>(frame.size.height));
}
}
std::string CocoaWindow::getClipboardText()
{
@autoreleasepool {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
NSString* text = [pasteboard stringForType:NSPasteboardTypeString];
if (text) {
return std::string([text UTF8String]);
}
return {};
}
}
std::string CocoaWindow::getPlatformType()
{
return "COCOA-MACOS";
}
int CocoaWindow::internalLoadMouseCursor(const ImagePtr& image, const Point& hotSpot)
{
@autoreleasepool {
int width = image->getWidth();
int height = image->getHeight();
const uint8_t* pixels = image->getPixelData();
NSBitmapImageRep* rep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:nullptr
pixelsWide:width
pixelsHigh:height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bytesPerRow:width * 4
bitsPerPixel:32];
memcpy([rep bitmapData], pixels, width * height * 4);
NSImage* nsImage = [[NSImage alloc] initWithSize:NSMakeSize(width, height)];
[nsImage addRepresentation:rep];
NSCursor* cursor = [[NSCursor alloc] initWithImage:nsImage
hotSpot:NSMakePoint(hotSpot.x, hotSpot.y)];
m_cursors.push_back(cursor);
return static_cast<int>(m_cursors.size()) - 1;
}
}
void CocoaWindow::handleKeyDown(unsigned short keyCode, unsigned int modifiers, const std::string& characters)
{
(void)characters;
updateModifiers(modifiers);
Fw::Key key = translateKeyCode(keyCode);
if (key >= Fw::KeyLast)
return;
const bool cmdPressed = (modifiers & NSEventModifierFlagCommand) != 0;
const auto keyIndex = static_cast<size_t>(key);
if (cmdPressed && key != Fw::KeyMeta && key != Fw::KeyCtrl && key != Fw::KeyAlt && key != Fw::KeyShift) {
m_commandKeyDown[keyIndex] = true;
}
processKeyDown(key);
}
void CocoaWindow::handleKeyUp(unsigned short keyCode, unsigned int modifiers)
{
updateModifiers(modifiers);
Fw::Key key = translateKeyCode(keyCode);
if (key < Fw::KeyLast) {
const auto keyIndex = static_cast<size_t>(key);
m_commandKeyDown[keyIndex] = false;
}
processKeyUp(key);
}
void CocoaWindow::handleFlagsChanged(unsigned int modifiers)
{
updateModifiers(modifiers);
bool cmdPressed = (modifiers & NSEventModifierFlagCommand) != 0;
bool cmdWasPressed = (m_lastModifiers & NSEventModifierFlagCommand) != 0;
if (cmdPressed != cmdWasPressed) {
if (cmdPressed)
processKeyDown(Fw::KeyMeta);
else
processKeyUp(Fw::KeyMeta);
if (!cmdPressed) {
for (size_t keyIndex = 0; keyIndex < m_commandKeyDown.size(); ++keyIndex) {
if (!m_commandKeyDown[keyIndex])
continue;
m_commandKeyDown[keyIndex] = false;
processKeyUp(static_cast<Fw::Key>(keyIndex));
}
}
}
bool optPressed = (modifiers & NSEventModifierFlagOption) != 0;
bool optWasPressed = (m_lastModifiers & NSEventModifierFlagOption) != 0;
if (optPressed != optWasPressed) {
if (optPressed)
processKeyDown(Fw::KeyAlt);
else
processKeyUp(Fw::KeyAlt);
}
bool ctrlPressed = (modifiers & NSEventModifierFlagControl) != 0;
bool ctrlWasPressed = (m_lastModifiers & NSEventModifierFlagControl) != 0;
if (ctrlPressed != ctrlWasPressed) {
if (ctrlPressed)
processKeyDown(Fw::KeyCtrl);
else
processKeyUp(Fw::KeyCtrl);
}
bool shiftPressed = (modifiers & NSEventModifierFlagShift) != 0;
bool shiftWasPressed = (m_lastModifiers & NSEventModifierFlagShift) != 0;
if (shiftPressed != shiftWasPressed) {
if (shiftPressed)
processKeyDown(Fw::KeyShift);
else
processKeyUp(Fw::KeyShift);
}
m_lastModifiers = modifiers;
}
void CocoaWindow::updateModifiers(unsigned int modifiers)
{
if (modifiers & NSEventModifierFlagCommand)
m_inputEvent.keyboardModifiers |= Fw::KeyboardPrimaryModifier;
else
m_inputEvent.keyboardModifiers &= ~Fw::KeyboardPrimaryModifier;
if (modifiers & NSEventModifierFlagOption)
m_inputEvent.keyboardModifiers |= Fw::KeyboardAltModifier;
else
m_inputEvent.keyboardModifiers &= ~Fw::KeyboardAltModifier;
if (modifiers & NSEventModifierFlagControl)
m_inputEvent.keyboardModifiers |= Fw::KeyboardCtrlModifier;
else
m_inputEvent.keyboardModifiers &= ~Fw::KeyboardCtrlModifier;
if (modifiers & NSEventModifierFlagShift)
m_inputEvent.keyboardModifiers |= Fw::KeyboardShiftModifier;
else
m_inputEvent.keyboardModifiers &= ~Fw::KeyboardShiftModifier;
}
void CocoaWindow::handleMouseButton(Fw::MouseButton button, bool pressed, const Point& position)
{
m_inputEvent.mousePos = position;
if (pressed) {
m_mouseButtonStates |= (1u << button);
m_inputEvent.reset(Fw::MousePressInputEvent);
m_inputEvent.mouseButton = button;
} else {
m_mouseButtonStates &= ~(1u << button);
m_inputEvent.reset(Fw::MouseReleaseInputEvent);
m_inputEvent.mouseButton = button;
}
if (m_onInputEvent)
m_onInputEvent(m_inputEvent);
}
void CocoaWindow::handleMouseMove(const Point& position)
{
m_inputEvent.reset(Fw::MouseMoveInputEvent);
m_inputEvent.mousePos = position;
if (m_onInputEvent)
m_onInputEvent(m_inputEvent);
}
void CocoaWindow::handleMouseScroll(int deltaX, int deltaY)
{
(void)deltaX;
m_inputEvent.reset(Fw::MouseWheelInputEvent);
if (deltaY != 0) {
m_inputEvent.wheelDirection = deltaY > 0 ? Fw::MouseWheelUp : Fw::MouseWheelDown;
if (m_onInputEvent)
m_onInputEvent(m_inputEvent);
}
}
void CocoaWindow::handleResize(int width, int height)
{
m_size = Size(width, height);
if (m_glContext) {
[m_glContext update];
}
if (m_onResize)
m_onResize(m_size);
}
void CocoaWindow::handleMove(int x, int y)
{
m_position = Point(x, y);
updateUnmaximizedCoords();
}
void CocoaWindow::handleClose()
{
if (m_onClose)
m_onClose();
}
void CocoaWindow::handleFocusChange(bool focused)
{
m_focused = focused;
if (!focused) {
releaseAllKeys();
}
}
void CocoaWindow::handleTextInput(const std::string& text)
{
if (text.empty()) return;
for (size_t i = 0; i < text.size(); ) {
unsigned char c = text[i];
int charLen = 1;
if ((c & 0x80) == 0) {
charLen = 1;
} else if ((c & 0xE0) == 0xC0) {
charLen = 2;
} else if ((c & 0xF0) == 0xE0) {
charLen = 3;
} else if ((c & 0xF8) == 0xF0) {
charLen = 4;
}
if (i + charLen <= text.size()) {
std::string character = text.substr(i, charLen);
m_inputEvent.reset(Fw::KeyTextInputEvent);
m_inputEvent.keyText = character;
if (m_onInputEvent)
m_onInputEvent(m_inputEvent);
}
i += charLen;
}
}
#endif

View file

@ -34,6 +34,9 @@ AndroidWindow window;
#elif defined __EMSCRIPTEN__
#include "browserwindow.h"
BrowserWindow window;
#elif defined __APPLE__
#include "cocoawindow.h"
CocoaWindow window;
#else
#include "x11window.h"
#include <framework/core/clock.h>
@ -86,20 +89,34 @@ void PlatformWindow::processKeyDown(Fw::Key keyCode)
if (keyCode == Fw::KeyUnknown)
return;
if (keyCode == Fw::KeyCtrl) {
m_inputEvent.keyboardModifiers |= Fw::KeyboardCtrlModifier;
return;
#if defined(__APPLE__)
} else if (keyCode == Fw::KeyMeta) {
m_inputEvent.keyboardModifiers |= Fw::KeyboardAltModifier;
if (keyCode == Fw::KeyMeta) {
m_inputEvent.keyboardModifiers |= Fw::KeyboardPrimaryModifier;
return;
#else
}
if (keyCode == Fw::KeyAlt) {
m_inputEvent.keyboardModifiers |= Fw::KeyboardAltModifier;
return;
#endif
}
if (keyCode == Fw::KeyCtrl) {
m_inputEvent.keyboardModifiers |= Fw::KeyboardCtrlModifier;
return;
}
#else
if (keyCode == Fw::KeyCtrl) {
m_inputEvent.keyboardModifiers |= Fw::KeyboardCtrlModifier;
m_inputEvent.keyboardModifiers |= Fw::KeyboardPrimaryModifier;
return;
}
if (keyCode == Fw::KeyAlt) {
m_inputEvent.keyboardModifiers |= Fw::KeyboardAltModifier;
return;
}
if (keyCode == Fw::KeyMeta) {
m_inputEvent.keyboardModifiers |= Fw::KeyboardMetaModifier;
return;
}
#endif
if (keyCode == Fw::KeyShift) {
m_inputEvent.keyboardModifiers |= Fw::KeyboardShiftModifier;
return;
@ -131,20 +148,34 @@ void PlatformWindow::processKeyUp(Fw::Key keyCode)
if (keyCode == Fw::KeyUnknown)
return;
if (keyCode == Fw::KeyCtrl) {
m_inputEvent.keyboardModifiers &= ~Fw::KeyboardCtrlModifier;
return;
#if defined(__APPLE__)
} else if (keyCode == Fw::KeyMeta) {
m_inputEvent.keyboardModifiers &= ~Fw::KeyboardAltModifier;
if (keyCode == Fw::KeyMeta) {
m_inputEvent.keyboardModifiers &= ~Fw::KeyboardPrimaryModifier;
return;
#else
}
if (keyCode == Fw::KeyAlt) {
m_inputEvent.keyboardModifiers &= ~Fw::KeyboardAltModifier;
return;
#endif
}
if (keyCode == Fw::KeyCtrl) {
m_inputEvent.keyboardModifiers &= ~Fw::KeyboardCtrlModifier;
return;
}
#else
if (keyCode == Fw::KeyCtrl) {
m_inputEvent.keyboardModifiers &= ~Fw::KeyboardCtrlModifier;
m_inputEvent.keyboardModifiers &= ~Fw::KeyboardPrimaryModifier;
return;
}
if (keyCode == Fw::KeyAlt) {
m_inputEvent.keyboardModifiers &= ~Fw::KeyboardAltModifier;
return;
}
if (keyCode == Fw::KeyMeta) {
m_inputEvent.keyboardModifiers &= ~Fw::KeyboardMetaModifier;
return;
}
#endif
if (keyCode == Fw::KeyShift) {
m_inputEvent.keyboardModifiers &= ~Fw::KeyboardShiftModifier;
return;
@ -211,4 +242,4 @@ void PlatformWindow::fireKeysPress()
keyInfo.lastTicks = now;
}
}
}
}

View file

@ -587,12 +587,12 @@ Fw::Key WIN32Window::retranslateVirtualKey(const WPARAM wParam, const LPARAM lPa
if (m_keyMap.contains(wParam))
key = m_keyMap[wParam];
// actually ignore alt/ctrl/shift keys, they is states are already stored in m_inputEvent.keyboardModifiers
// actually ignore modifier keys, their states are already stored in m_inputEvent.keyboardModifiers
#if defined(__APPLE__)
if (key == Fw::KeyMeta || key == Fw::KeyCtrl || key == Fw::KeyShift)
key = Fw::KeyUnknown;
#else
if (key == Fw::KeyAlt || key == Fw::KeyCtrl || key == Fw::KeyShift)
if (key == Fw::KeyAlt || key == Fw::KeyCtrl || key == Fw::KeyShift || key == Fw::KeyMeta)
key = Fw::KeyUnknown;
#endif
@ -604,8 +604,10 @@ Fw::Key WIN32Window::retranslateVirtualKey(const WPARAM wParam, const LPARAM lPa
LRESULT WIN32Window::windowProc(const HWND hWnd, const uint32_t uMsg, const WPARAM wParam, const LPARAM lParam)
{
m_inputEvent.keyboardModifiers = 0;
if (IsKeyDown(VK_CONTROL))
if (IsKeyDown(VK_CONTROL)) {
m_inputEvent.keyboardModifiers |= Fw::KeyboardCtrlModifier;
m_inputEvent.keyboardModifiers |= Fw::KeyboardPrimaryModifier;
}
if (IsKeyDown(VK_SHIFT))
m_inputEvent.keyboardModifiers |= Fw::KeyboardShiftModifier;
#if defined(__APPLE__)
@ -615,6 +617,8 @@ LRESULT WIN32Window::windowProc(const HWND hWnd, const uint32_t uMsg, const WPAR
if (IsKeyDown(VK_MENU))
m_inputEvent.keyboardModifiers |= Fw::KeyboardAltModifier;
#endif
if (IsKeyDown(VK_LWIN) || IsKeyDown(VK_RWIN))
m_inputEvent.keyboardModifiers |= Fw::KeyboardMetaModifier;
bool signalKeyEvent = false;
switch (uMsg) {

View file

@ -272,7 +272,7 @@ SoundSourcePtr SoundManager::createSoundSource(const std::string& name)
streamSource->setRelative(true);
streamSource->setPosition(Point(-128, 0));
combinedSource->addSource(streamSource);
m_streamFiles[streamSource] = g_asyncDispatcher.submit_task([=]() -> SoundFilePtr {
m_streamFiles[streamSource] = g_asyncDispatcher->submit_task([=]() -> SoundFilePtr {
stdext::timer a;
try {
return SoundFile::loadSoundFile(filename);
@ -287,7 +287,7 @@ SoundSourcePtr SoundManager::createSoundSource(const std::string& name)
streamSource->setRelative(true);
streamSource->setPosition(Point(128, 0));
combinedSource->addSource(streamSource);
m_streamFiles[streamSource] = g_asyncDispatcher.submit_task([=]() -> SoundFilePtr {
m_streamFiles[streamSource] = g_asyncDispatcher->submit_task([=]() -> SoundFilePtr {
try {
return SoundFile::loadSoundFile(filename);
} catch (std::exception& e) {
@ -299,7 +299,7 @@ SoundSourcePtr SoundManager::createSoundSource(const std::string& name)
source = combinedSource;
#else
const auto& streamSource = std::make_shared<StreamSoundSource>();
m_streamFiles[streamSource] = g_asyncDispatcher.submit_task([=]() -> SoundFilePtr {
m_streamFiles[streamSource] = g_asyncDispatcher->submit_task([=]() -> SoundFilePtr {
try {
return SoundFile::loadSoundFile(filename);
} catch (std::exception& e) {

View file

@ -1380,6 +1380,8 @@ bool UITextEdit::onKeyPress(const uint8_t keyCode, const int keyboardModifiers,
if (UIWidget::onKeyPress(keyCode, keyboardModifiers, autoRepeatTicks))
return true;
const bool primaryOnly = Fw::isPrimaryModifierOnly(keyboardModifiers);
if (keyboardModifiers == Fw::KeyboardNoModifier) {
if (keyCode == Fw::KeyDelete && getProp(PropEditable)) {
if (hasSelection() || !m_text.empty()) {
@ -1463,7 +1465,7 @@ bool UITextEdit::onKeyPress(const uint8_t keyCode, const int keyboardModifiers,
moveCursorVertically(false);
return true;
}
} else if (keyboardModifiers == Fw::KeyboardCtrlModifier) {
} else if (primaryOnly) {
if (keyCode == Fw::KeyV && getProp(PropEditable)) {
paste(g_window.getClipboardText());
return true;

View file

@ -35,6 +35,7 @@
#include "framework/html/htmlmanager.h"
#include "framework/html/htmlnode.h"
#include "framework/otml/otmlnode.h"
#include "framework/graphics/coordsbuffer.h"
#include <framework/platform/platformwindow.h>
#include <framework/util/stats.h>
@ -58,6 +59,8 @@ UIWidget::UIWidget()
m_positions.set(Unit::Auto);
m_clickTimer.stop();
m_textUnderline = std::make_shared<CoordsBuffer>();
initBaseStyle();
initText();
initImage();

View file

@ -26,6 +26,7 @@
#include <framework/core/timer.h>
#include <framework/graphics/declarations.h>
#include <framework/graphics/coordsbuffer.h>
#include <framework/html/declarations.h>
#include <framework/luaengine/luaobject.h>

View file

@ -621,6 +621,7 @@ void UIWidget::parseBaseStyle(const OTMLNodePtr& styleNode)
setFloat(type);
} else if (node->tag() == "clear") {
auto v = node->value<std::string>();
stdext::tolower(v);
ClearType clear = ClearType::None;
if (v == "left") clear = ClearType::Left;
else if (v == "right") clear = ClearType::Right;

View file

@ -115,5 +115,10 @@ struct fmt::formatter<TPoint<T>, char> {
}
};
using Point = TPoint<int>;
using PointF = TPoint<float>;
namespace Fw {
using Point = TPoint<int>;
using PointF = TPoint<float>;
}
using Fw::Point;
using Fw::PointF;

View file

@ -134,5 +134,10 @@ private:
T wd, ht;
};
using Size = TSize<int>;
using SizeF = TSize<float>;
namespace Fw {
using Size = TSize<int>;
using SizeF = TSize<float>;
}
using Fw::Size;
using Fw::SizeF;

View file

@ -38,6 +38,16 @@ function(otclient_add_gtest TARGET_NAME)
FRAMEWORK_XML
)
if(ASAN_ENABLED)
if(MSVC)
target_compile_options(${TARGET_NAME} PRIVATE /fsanitize=address)
target_link_options(${TARGET_NAME} PRIVATE /fsanitize=address)
else()
target_compile_options(${TARGET_NAME} PRIVATE -fsanitize=address)
target_link_options(${TARGET_NAME} PRIVATE -fsanitize=address)
endif()
endif()
if(MSVC)
target_compile_options(${TARGET_NAME} PRIVATE /utf-8)
if(BUILD_STATIC_LIBRARY)

5
vcpkg-configuration.json Normal file
View file

@ -0,0 +1,5 @@
{
"overlay-ports": [
"overlay-ports"
]
}