diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4d404fec2..b1b8f11e3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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]
diff --git a/.github/workflows/reusable-build-macos.yml b/.github/workflows/reusable-build-macos.yml
new file mode 100644
index 000000000..3eb968909
--- /dev/null
+++ b/.github/workflows/reusable-build-macos.yml
@@ -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
diff --git a/.gitignore b/.gitignore
index 5fff6df26..3aea3c03e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -196,6 +196,7 @@ pip-log.txt
#################
/otclient
+/OTClient.app
/modules/otclientrc.lua
src/framework/graphics/dx/
diff --git a/CMakePresets.json b/CMakePresets.json
index c6b00e7ab..c9833adcd 100644
--- a/CMakePresets.json
+++ b/CMakePresets.json
@@ -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",
diff --git a/cmake/Info.plist.in b/cmake/Info.plist.in
new file mode 100644
index 000000000..ffa9d2f9b
--- /dev/null
+++ b/cmake/Info.plist.in
@@ -0,0 +1,32 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleDisplayName
+ @MACOSX_BUNDLE_DISPLAY_NAME@
+ CFBundleExecutable
+ @MACOSX_BUNDLE_EXECUTABLE_NAME@
+ CFBundleIdentifier
+ @MACOSX_BUNDLE_IDENTIFIER@
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ @MACOSX_BUNDLE_BUNDLE_NAME@
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ @MACOSX_BUNDLE_SHORT_VERSION_STRING@
+ CFBundleVersion
+ @MACOSX_BUNDLE_VERSION@
+ CFBundleIconFile
+ @MACOSX_BUNDLE_ICON_FILE@
+ LSMinimumSystemVersion
+ @MACOSX_BUNDLE_MINIMUM_SYSTEM_VERSION@
+ NSHumanReadableCopyright
+ @MACOSX_BUNDLE_COPYRIGHT@
+ NSPrincipalClass
+ NSApplication
+
+
diff --git a/cmake/OTClient.icns b/cmake/OTClient.icns
new file mode 100644
index 000000000..4a52025bb
Binary files /dev/null and b/cmake/OTClient.icns differ
diff --git a/data/images/clienticon.png b/data/images/clienticon.png
index 5b9c6de13..80fe7d693 100644
Binary files a/data/images/clienticon.png and b/data/images/clienticon.png differ
diff --git a/modules/corelib/const.lua b/modules/corelib/const.lua
index 89cce17ee..f5643f833 100644
--- a/modules/corelib/const.lua
+++ b/modules/corelib/const.lua
@@ -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
diff --git a/modules/corelib/corelib.otmod b/modules/corelib/corelib.otmod
index 14061a8de..b33e33fef 100644
--- a/modules/corelib/corelib.otmod
+++ b/modules/corelib/corelib.otmod
@@ -34,5 +34,6 @@ Module
dofile 'objectpool'
Keybind.init()
-
+ initPlatformKeyDescs()
+
@onUnload: Keybind.terminate()
diff --git a/modules/corelib/keybind.lua b/modules/corelib/keybind.lua
index 7340f2c54..a2982b171 100644
--- a/modules/corelib/keybind.lua
+++ b/modules/corelib/keybind.lua
@@ -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])
diff --git a/modules/corelib/keyboard.lua b/modules/corelib/keyboard.lua
index 657c0d29c..d5079e219 100644
--- a/modules/corelib/keyboard.lua
+++ b/modules/corelib/keyboard.lua
@@ -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
diff --git a/modules/corelib/net.lua b/modules/corelib/net.lua
index 1c24cca04..0e99f6810 100644
--- a/modules/corelib/net.lua
+++ b/modules/corelib/net.lua
@@ -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.')
diff --git a/modules/game_analyser/classes/BossCooldown.lua b/modules/game_analyser/classes/BossCooldown.lua
index f0a539f6f..481bd45be 100644
--- a/modules/game_analyser/classes/BossCooldown.lua
+++ b/modules/game_analyser/classes/BossCooldown.lua
@@ -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
diff --git a/modules/game_console/console.lua b/modules/game_console/console.lua
index fef417e06..899f63622 100644
--- a/modules/game_console/console.lua
+++ b/modules/game_console/console.lua
@@ -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)
diff --git a/modules/game_hotkeys/hotkeys_manager.lua b/modules/game_hotkeys/hotkeys_manager.lua
index 75d62a17f..f2843d9c1 100644
--- a/modules/game_hotkeys/hotkeys_manager.lua
+++ b/modules/game_hotkeys/hotkeys_manager.lua
@@ -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
diff --git a/modules/game_interface/gameinterface.lua b/modules/game_interface/gameinterface.lua
index 2013bb0f4..13a09f1e3 100644
--- a/modules/game_interface/gameinterface.lua
+++ b/modules/game_interface/gameinterface.lua
@@ -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
diff --git a/modules/game_walk/walk.lua b/modules/game_walk/walk.lua
index db63fedab..d27f99e86 100644
--- a/modules/game_walk/walk.lua
+++ b/modules/game_walk/walk.lua
@@ -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()
diff --git a/modules/gamelib/ui/uiminimap.lua b/modules/gamelib/ui/uiminimap.lua
index a69b254dd..24a32594f 100644
--- a/modules/gamelib/ui/uiminimap.lua
+++ b/modules/gamelib/ui/uiminimap.lua
@@ -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
diff --git a/overlay-ports/inih/CMakeLists.txt b/overlay-ports/inih/CMakeLists.txt
new file mode 100644
index 000000000..75945834a
--- /dev/null
+++ b/overlay-ports/inih/CMakeLists.txt
@@ -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
+ $
+ $
+)
+
+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
+ $
+ $
+ $
+ )
+ 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()
diff --git a/overlay-ports/inih/portfile.cmake b/overlay-ports/inih/portfile.cmake
new file mode 100644
index 000000000..5afbfba7b
--- /dev/null
+++ b/overlay-ports/inih/portfile.cmake
@@ -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)
diff --git a/overlay-ports/inih/unofficial-inihConfig.cmake.in b/overlay-ports/inih/unofficial-inihConfig.cmake.in
new file mode 100644
index 000000000..0d39426ed
--- /dev/null
+++ b/overlay-ports/inih/unofficial-inihConfig.cmake.in
@@ -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 ####
+###################
diff --git a/overlay-ports/inih/usage b/overlay-ports/inih/usage
new file mode 100644
index 000000000..7d6fa57c2
--- /dev/null
+++ b/overlay-ports/inih/usage
@@ -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
diff --git a/overlay-ports/inih/vcpkg.json b/overlay-ports/inih/vcpkg.json
new file mode 100644
index 000000000..a56971e1d
--- /dev/null
+++ b/overlay-ports/inih/vcpkg.json
@@ -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"
+ }
+ }
+}
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index d512752ea..c7dcecec9 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -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
+ $<$,$>>: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"
+ "$/../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()
diff --git a/src/client/map.cpp b/src/client/map.cpp
index aca796b40..3d6554b9e 100644
--- a/src/client/map.cpp
+++ b/src/client/map.cpp
@@ -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));
});
diff --git a/src/client/mapview.cpp b/src/client/mapview.cpp
index d8129bf11..cb4d95f26 100644
--- a/src/client/mapview.cpp
+++ b/src/client/mapview.cpp
@@ -46,7 +46,7 @@
MapView::MapView() : m_lightView(std::make_unique(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 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);
}));
}
diff --git a/src/client/spritemanager.cpp b/src/client/spritemanager.cpp
index 5fdee13da..20fd48f96 100644
--- a/src/client/spritemanager.cpp
+++ b/src/client/spritemanager.cpp
@@ -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(g_resources.openFile(m_lastFileName));
diff --git a/src/client/thingtype.cpp b/src/client/thingtype.cpp
index 698c7d7a3..4e4b6a5a3 100644
--- a/src/client/thingtype.cpp
+++ b/src/client/thingtype.cpp
@@ -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;
diff --git a/src/framework/const.h b/src/framework/const.h
index 0cb6a3be7..d7cd1dd07 100644
--- a/src/framework/const.h
+++ b/src/framework/const.h
@@ -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,
diff --git a/src/framework/core/application.cpp b/src/framework/core/application.cpp
index 314800079..f60e8930d 100644
--- a/src/framework/core/application.cpp
+++ b/src/framework/core/application.cpp
@@ -22,6 +22,7 @@
#include "application.h"
+#include "asyncdispatcher.h"
#include
#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);
diff --git a/src/framework/core/asyncdispatcher.cpp b/src/framework/core/asyncdispatcher.cpp
index 5622fb33b..df22ea863 100644
--- a/src/framework/core/asyncdispatcher.cpp
+++ b/src/framework/core/asyncdispatcher.cpp
@@ -38,4 +38,4 @@ uint8_t getThreadCount() {
return std::clamp(std::thread::hardware_concurrency() - 1, MIN_THREADS, MAX_THREADS);
}
-AsyncDispatcher g_asyncDispatcher{ getThreadCount() };
+std::unique_ptr g_asyncDispatcher = std::make_unique(getThreadCount());
diff --git a/src/framework/core/asyncdispatcher.h b/src/framework/core/asyncdispatcher.h
index 62e4f03a7..10e52f5d0 100644
--- a/src/framework/core/asyncdispatcher.h
+++ b/src/framework/core/asyncdispatcher.h
@@ -25,7 +25,8 @@
#include
#include
+#include
using AsyncDispatcher = decltype(BS::thread_pool{ std::size_t{} });
-extern AsyncDispatcher g_asyncDispatcher;
+extern std::unique_ptr g_asyncDispatcher;
diff --git a/src/framework/core/eventdispatcher.cpp b/src/framework/core/eventdispatcher.cpp
index 8b79bb1d6..fbf720d75 100644
--- a/src/framework/core/eventdispatcher.cpp
+++ b/src/framework/core/eventdispatcher.cpp
@@ -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());
}
}
diff --git a/src/framework/core/graphicalapplication.cpp b/src/framework/core/graphicalapplication.cpp
index f38897900..420bdd058 100644
--- a/src/framework/core/graphicalapplication.cpp
+++ b/src/framework/core/graphicalapplication.cpp
@@ -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 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 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>(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();
diff --git a/src/framework/core/modulemanager.cpp b/src/framework/core/modulemanager.cpp
index 489071852..de73471ac 100644
--- a/src/framework/core/modulemanager.cpp
+++ b/src/framework/core/modulemanager.cpp
@@ -220,6 +220,6 @@ void ModuleManager::enableAutoReload() {
return;
processing.store(true);
- g_asyncDispatcher.detach_task(action);
+ g_asyncDispatcher->detach_task(action);
}, 500);
}
\ No newline at end of file
diff --git a/src/framework/core/resourcemanager.cpp b/src/framework/core/resourcemanager.cpp
index 2a080faf9..7a16a5173 100644
--- a/src/framework/core/resourcemanager.cpp
+++ b/src/framework/core/resourcemanager.cpp
@@ -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& 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& 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& args) { // c
}
}
+ if (ec) {
+ return false;
+ }
+
if (binary == m_binaryPath)
return false;
diff --git a/src/framework/graphics/drawpool.cpp b/src/framework/graphics/drawpool.cpp
index a374d2b1b..229b28ca1 100644
--- a/src/framework/graphics/drawpool.cpp
+++ b/src/framework/graphics/drawpool.cpp
@@ -24,6 +24,7 @@
#include "painter.h"
#include "textureatlas.h"
+#include "coordsbuffer.h"
DrawPool* DrawPool::create(const DrawPoolType type)
{
diff --git a/src/framework/graphics/framebuffer.cpp b/src/framework/graphics/framebuffer.cpp
index f175dbee7..d8265bacc 100644
--- a/src/framework/graphics/framebuffer.cpp
+++ b/src/framework/graphics/framebuffer.cpp
@@ -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();
diff --git a/src/framework/graphics/glutil.h b/src/framework/graphics/glutil.h
index a39076983..d5665f652 100644
--- a/src/framework/graphics/glutil.h
+++ b/src/framework/graphics/glutil.h
@@ -27,6 +27,10 @@
#define GL_APICALL
#define EGLAPI
#include
+#elif defined(__APPLE__)
+#define GLEW_STATIC
+#define GLEW_NO_GLU
+#include
#else
#ifndef _MSC_VER
#define GLEW_STATIC
diff --git a/src/framework/luaengine/luavaluecasts.h b/src/framework/luaengine/luavaluecasts.h
index 6f2481b10..73e3fdeda 100644
--- a/src/framework/luaengine/luavaluecasts.h
+++ b/src/framework/luaengine/luavaluecasts.h
@@ -29,6 +29,11 @@
#include "framework/platform/staticdata.h"
+#include
+#include
+#include
+#include
+
template
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
+ inline bool luavalue_cast_integer_from_double(const int index, T& v)
+ {
+ static_assert(std::is_integral_v);
+
+ double d;
+ if (!luavalue_cast(index, d))
+ return false;
+ if (!std::isfinite(d) || std::trunc(d) != d)
+ return false;
+
+ const double typeMin = static_cast(std::numeric_limits::lowest());
+ const double typeMax = static_cast(std::numeric_limits::max());
+ const double exactMin = std::numeric_limits::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(d);
+ return true;
+ }
+}
+
// float
inline int push_luavalue(const float f) { push_luavalue(static_cast(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(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(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;
@@ -100,6 +137,7 @@ using lua_unsigned_long = lua_u64;
static_assert(sizeof(lua_u64) == 8, "lua_u64 must be 64-bit");
+template requires (!std::is_same_v)
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 requires (!std::is_same_v)
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(temp);
- return r;
+ return detail::luavalue_cast_integer_from_double(index, v);
}
-template, int> = 0>
+template requires (!std::is_same_v)
inline int push_luavalue(lua_u64 v)
{
push_luavalue(static_cast(v));
return 1;
}
-template, int> = 0>
+template requires (!std::is_same_v && !std::is_same_v)
inline bool luavalue_cast(const int idx, lua_u64& v)
{
- double d;
- const bool r = luavalue_cast(idx, d);
- v = static_cast(d);
- return r;
+ return detail::luavalue_cast_integer_from_double(idx, v);
}
// string
diff --git a/src/framework/net/httplogin.cpp b/src/framework/net/httplogin.cpp
index 0727b8e54..36dac70f8 100644
--- a/src/framework/net/httplogin.cpp
+++ b/src/framework/net/httplogin.cpp
@@ -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
+
#include "httplogin.h"
#include
#include
@@ -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;
-}
\ No newline at end of file
+}
diff --git a/src/framework/net/httplogin.h b/src/framework/net/httplogin.h
index becf82599..04bd6831e 100644
--- a/src/framework/net/httplogin.h
+++ b/src/framework/net/httplogin.h
@@ -22,11 +22,11 @@
#pragma once
-#ifndef CPPHTTPLIB_OPENSSL_SUPPORT
-# define CPPHTTPLIB_OPENSSL_SUPPORT
-#endif
+#include
+#include
+#include
+
#include
-#include
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;
diff --git a/src/framework/platform/cocoaview.h b/src/framework/platform/cocoaview.h
new file mode 100644
index 000000000..86f452b02
--- /dev/null
+++ b/src/framework/platform/cocoaview.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2010-2025 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
+#import
+
+#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
+@property (nonatomic, assign) CocoaWindow* platformWindow;
+@end
+
+#endif
diff --git a/src/framework/platform/cocoaview.mm b/src/framework/platform/cocoaview.mm
new file mode 100644
index 000000000..cfc2945d7
--- /dev/null
+++ b/src/framework/platform/cocoaview.mm
@@ -0,0 +1,271 @@
+/*
+ * Copyright (c) 2010-2025 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
+
+#define Point MacPoint
+#define Size MacSize
+#define Rect MacRect
+
+#import
+
+#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(loc.x), static_cast(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(_scrollAccumX);
+ int intDY = static_cast(_scrollAccumY);
+ if (intDX != 0 || intDY != 0) {
+ _platformWindow->handleMouseScroll(intDX, intDY);
+ _scrollAccumX -= intDX;
+ _scrollAccumY -= intDY;
+ }
+ } else {
+ _platformWindow->handleMouseScroll(static_cast(deltaX), static_cast(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(contentRect.size.width),
+ static_cast(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(screenFrame.origin.y + screenFrame.size.height - frame.origin.y - frame.size.height);
+ _platformWindow->handleMove(static_cast(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
diff --git a/src/framework/platform/cocoawindow.h b/src/framework/platform/cocoawindow.h
new file mode 100644
index 000000000..d84d768f1
--- /dev/null
+++ b/src/framework/platform/cocoawindow.h
@@ -0,0 +1,296 @@
+/*
+ * Copyright (c) 2010-2025 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
+
+#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 m_cursors;
+ NSCursor* m_currentCursor;
+ NSCursor* m_defaultCursor;
+ bool m_cursorHidden;
+ bool m_cursorInWindow;
+
+ unsigned int m_lastModifiers;
+ std::array m_commandKeyDown{};
+};
diff --git a/src/framework/platform/cocoawindow.mm b/src/framework/platform/cocoawindow.mm
new file mode 100644
index 000000000..2d3cf30dd
--- /dev/null
+++ b/src/framework/platform/cocoawindow.mm
@@ -0,0 +1,824 @@
+/*
+ * Copyright (c) 2010-2025 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
+
+#define Point MacPoint
+#define Size MacSize
+#define Rect MacRect
+
+#define GL_SILENCE_DEPRECATION
+#import
+#import
+#import
+#import
+
+#undef Point
+#undef Size
+#undef Rect
+
+#include "cocoawindow.h"
+#include "cocoaview.h"
+#include
+#include
+
+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(contentRect.size.width),
+ static_cast(contentRect.size.height));
+
+ NSRect windowFrame = [m_window frame];
+ NSScreen* screen = [m_window screen];
+ if (screen) {
+ NSRect screenFrame = [screen frame];
+ m_position = Point(static_cast(windowFrame.origin.x),
+ static_cast(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(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(frame.size.width), static_cast(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(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(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(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(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
diff --git a/src/framework/platform/platformwindow.cpp b/src/framework/platform/platformwindow.cpp
index 4ada4cdff..3816c3398 100644
--- a/src/framework/platform/platformwindow.cpp
+++ b/src/framework/platform/platformwindow.cpp
@@ -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
@@ -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;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/framework/platform/win32window.cpp b/src/framework/platform/win32window.cpp
index d4b2f2749..c41b821a6 100644
--- a/src/framework/platform/win32window.cpp
+++ b/src/framework/platform/win32window.cpp
@@ -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) {
diff --git a/src/framework/sound/soundmanager.cpp b/src/framework/sound/soundmanager.cpp
index fe3f98f05..09edec00e 100644
--- a/src/framework/sound/soundmanager.cpp
+++ b/src/framework/sound/soundmanager.cpp
@@ -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();
- 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) {
diff --git a/src/framework/ui/uitextedit.cpp b/src/framework/ui/uitextedit.cpp
index 1ac33616e..361c4ac70 100644
--- a/src/framework/ui/uitextedit.cpp
+++ b/src/framework/ui/uitextedit.cpp
@@ -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;
diff --git a/src/framework/ui/uiwidget.cpp b/src/framework/ui/uiwidget.cpp
index 420071932..bc5a804af 100644
--- a/src/framework/ui/uiwidget.cpp
+++ b/src/framework/ui/uiwidget.cpp
@@ -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
#include
@@ -58,6 +59,8 @@ UIWidget::UIWidget()
m_positions.set(Unit::Auto);
m_clickTimer.stop();
+ m_textUnderline = std::make_shared();
+
initBaseStyle();
initText();
initImage();
diff --git a/src/framework/ui/uiwidget.h b/src/framework/ui/uiwidget.h
index 54d133550..7a702dfc8 100644
--- a/src/framework/ui/uiwidget.h
+++ b/src/framework/ui/uiwidget.h
@@ -26,6 +26,7 @@
#include
#include
+#include
#include
#include
diff --git a/src/framework/ui/uiwidgetbasestyle.cpp b/src/framework/ui/uiwidgetbasestyle.cpp
index 3e2aad6db..a15ef6f1a 100644
--- a/src/framework/ui/uiwidgetbasestyle.cpp
+++ b/src/framework/ui/uiwidgetbasestyle.cpp
@@ -621,6 +621,7 @@ void UIWidget::parseBaseStyle(const OTMLNodePtr& styleNode)
setFloat(type);
} else if (node->tag() == "clear") {
auto v = node->value();
+ stdext::tolower(v);
ClearType clear = ClearType::None;
if (v == "left") clear = ClearType::Left;
else if (v == "right") clear = ClearType::Right;
diff --git a/src/framework/util/point.h b/src/framework/util/point.h
index c3bc1e84d..c99d7d55e 100644
--- a/src/framework/util/point.h
+++ b/src/framework/util/point.h
@@ -115,5 +115,10 @@ struct fmt::formatter, char> {
}
};
-using Point = TPoint;
-using PointF = TPoint;
+namespace Fw {
+ using Point = TPoint;
+ using PointF = TPoint;
+}
+
+using Fw::Point;
+using Fw::PointF;
diff --git a/src/framework/util/size.h b/src/framework/util/size.h
index d8cc0e054..5f21f9212 100644
--- a/src/framework/util/size.h
+++ b/src/framework/util/size.h
@@ -134,5 +134,10 @@ private:
T wd, ht;
};
-using Size = TSize;
-using SizeF = TSize;
+namespace Fw {
+ using Size = TSize;
+ using SizeF = TSize;
+}
+
+using Fw::Size;
+using Fw::SizeF;
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 38d7c60b7..214772f3a 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -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)
diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json
new file mode 100644
index 000000000..3deb4e36c
--- /dev/null
+++ b/vcpkg-configuration.json
@@ -0,0 +1,5 @@
+{
+ "overlay-ports": [
+ "overlay-ports"
+ ]
+}