ref: 1cf4200051217c229c3cc69cf71404f8a6886489
parent: 1d9048c8a6a6388607799384d4912eec00e88c5d
parent: bcd883e767526e7e5f4a8fae0c68853ee8ea1e00
author: Gabriel Ravier <[email protected]>
date: Sun Apr 5 15:33:13 EDT 2020
Merge branch 'portable' into improvePerformance2
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -439,11 +439,11 @@
if (PKG_CONFIG_STATIC_LIBS)
message(STATUS "Using system GLFW3 (pkg-config, static)")
target_compile_options(CSE2 PRIVATE ${glfw3_STATIC_CFLAGS})
- target_link_libraries(CSE2 PRIVATE ${glfw3_STATIC_LINK_LIBRARIES})
+ target_link_libraries(CSE2 PRIVATE ${glfw3_STATIC_LDFLAGS})
else()
message(STATUS "Using system GLFW3 (pkg-config, dynamic)")
target_compile_options(CSE2 PRIVATE ${glfw3_CFLAGS})
- target_link_libraries(CSE2 PRIVATE ${glfw3_LINK_LIBRARIES})
+ target_link_libraries(CSE2 PRIVATE ${glfw3_LDFLAGS})
endif()
elseif(TARGET glfw)
# CMake
@@ -475,11 +475,11 @@
if (PKG_CONFIG_STATIC_LIBS)
message(STATUS "Using system SDL2 (pkg-config, static)")
target_compile_options(CSE2 PRIVATE ${sdl2_STATIC_CFLAGS})
- target_link_libraries(CSE2 PRIVATE ${sdl2_STATIC_LINK_LIBRARIES})
+ target_link_libraries(CSE2 PRIVATE ${sdl2_STATIC_LDFLAGS})
else()
message(STATUS "Using system SDL2 (pkg-config, dynamic)")
target_compile_options(CSE2 PRIVATE ${sdl2_CFLAGS})
- target_link_libraries(CSE2 PRIVATE ${sdl2_LINK_LIBRARIES})
+ target_link_libraries(CSE2 PRIVATE ${sdl2_LDFLAGS})
endif()
elseif(TARGET SDL2::SDL2)
# CMake-generated config (Arch, vcpkg, Raspbian)
@@ -519,11 +519,11 @@
if (PKG_CONFIG_STATIC_LIBS)
message(STATUS "Using system FreeType (pkg-config, static)")
target_compile_options(CSE2 PRIVATE ${freetype2_STATIC_CFLAGS})
- target_link_libraries(CSE2 PRIVATE ${freetype2_STATIC_LINK_LIBRARIES})
+ target_link_libraries(CSE2 PRIVATE ${freetype2_STATIC_LDFLAGS})
else()
message(STATUS "Using system FreeType (pkg-config, dynamic)")
target_compile_options(CSE2 PRIVATE ${freetype2_CFLAGS})
- target_link_libraries(CSE2 PRIVATE ${freetype2_LINK_LIBRARIES})
+ target_link_libraries(CSE2 PRIVATE ${freetype2_LDFLAGS})
endif()
elseif(FREETYPE_FOUND)
message(STATUS "Using system FreeType (CMake)")
--- a/DoConfig/CMakeLists.txt
+++ b/DoConfig/CMakeLists.txt
@@ -95,11 +95,11 @@
if (PKG_CONFIG_STATIC_LIBS)
message(STATUS "Using system GLFW3 (pkg-config, static)")
target_compile_options(DoConfig PRIVATE ${glfw3_STATIC_CFLAGS})
- target_link_libraries(DoConfig PRIVATE ${glfw3_STATIC_LINK_LIBRARIES})
+ target_link_libraries(DoConfig PRIVATE ${glfw3_STATIC_LDFLAGS})
else()
message(STATUS "Using system GLFW3 (pkg-config, dynamic)")
target_compile_options(DoConfig PRIVATE ${glfw3_CFLAGS})
- target_link_libraries(DoConfig PRIVATE ${glfw3_LINK_LIBRARIES})
+ target_link_libraries(DoConfig PRIVATE ${glfw3_LDFLAGS})
endif()
elseif(TARGET glfw)
# CMake
--- a/external/glad/CMakeLists.txt
+++ b/external/glad/CMakeLists.txt
@@ -4,7 +4,7 @@
project(glad LANGUAGES C)
-add_library(glad
+add_library(glad STATIC
"include/glad/glad.h"
"include/KHR/khrplatform.h"
"src/glad.c"
--- a/src/Backends/Audio/SoftwareMixer.cpp
+++ b/src/Backends/Audio/SoftwareMixer.cpp
@@ -154,7 +154,7 @@
const float sample2 = (sound->samples[(size_t)sound->position + 1] - 128.0f) / 128.0f;
// Perform linear interpolation
- const float interpolated_sample = sample1 + ((sample2 - sample1) * fmod((float)sound->position, 1.0f));
+ const float interpolated_sample = sample1 + ((sample2 - sample1) * fmod(sound->position, 1.0));
*steam_pointer++ += interpolated_sample * sound->volume_l;
*steam_pointer++ += interpolated_sample * sound->volume_r;
--- a/src/Backends/GLFW3/Controller.cpp
+++ b/src/Backends/GLFW3/Controller.cpp
@@ -2,6 +2,7 @@
#include <stddef.h>
#include <stdio.h>
+#include <stdlib.h>
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
@@ -12,9 +13,9 @@
static BOOL joystick_connected;
static int connected_joystick_id;
-static int joystick_neutral_x;
-static int joystick_neutral_y;
+static float *axis_neutrals;
+
static void JoystickCallback(int joystick_id, int event)
{
switch (event)
@@ -24,10 +25,13 @@
if (!joystick_connected)
{
- int total_axis;
- const float *axis = glfwGetJoystickAxes(joystick_id, &total_axis);
+ int total_axes;
+ const float *axes = glfwGetJoystickAxes(joystick_id, &total_axes);
- if (total_axis >= 2)
+ int total_buttons;
+ const unsigned char *buttons = glfwGetJoystickButtons(joystick_id, &total_buttons);
+
+ if (total_axes >= 2 && total_buttons >= 6)
{
#if GLFW_VERSION_MAJOR > 3 || (GLFW_VERSION_MAJOR == 3 && GLFW_VERSION_MINOR >= 3)
if (glfwJoystickIsGamepad(joystick_id) == GLFW_TRUE) // Avoid selecting things like laptop touchpads
@@ -37,9 +41,11 @@
joystick_connected = TRUE;
connected_joystick_id = joystick_id;
- // Reset default stick positions (this is performed in ResetJoystickStatus in vanilla Cave Story
- joystick_neutral_x = axis[0];
- joystick_neutral_y = axis[1];
+ // Set up neutral axes
+ axis_neutrals = (float*)malloc(sizeof(float) * total_axes);
+
+ for (int i = 0; i < total_axes; ++i)
+ axis_neutrals[i] = axes[i];
}
}
}
@@ -51,6 +57,8 @@
{
printf("Joystick #%d disconnected\n", connected_joystick_id);
joystick_connected = FALSE;
+
+ free(axis_neutrals);
}
break;
@@ -76,8 +84,9 @@
joystick_connected = FALSE;
connected_joystick_id = 0;
- joystick_neutral_x = 0;
- joystick_neutral_y = 0;
+
+ free(axis_neutrals);
+ axis_neutrals = NULL;
}
BOOL ControllerBackend_GetJoystickStatus(JOYSTICK_STATUS *status)
@@ -85,29 +94,79 @@
if (!joystick_connected)
return FALSE;
- // Read axis
- int total_axis;
- const float *axis = glfwGetJoystickAxes(connected_joystick_id, &total_axis);
+ const size_t button_limit = sizeof(status->bButton) / sizeof(status->bButton[0]);
- status->bLeft = axis[0] < joystick_neutral_x - DEADZONE;
- status->bRight = axis[0] > joystick_neutral_x + DEADZONE;
- status->bUp = axis[1] < joystick_neutral_y - DEADZONE;
- status->bDown = axis[1] > joystick_neutral_y + DEADZONE;
-
- // Read buttons
int total_buttons;
const unsigned char *buttons = glfwGetJoystickButtons(connected_joystick_id, &total_buttons);
- // The original `Input.cpp` assumed there were 32 buttons (because of DirectInput's `DIJOYSTATE` struct)
- if (total_buttons > 32)
- total_buttons = 32;
+ int total_axes;
+ const float *axes = glfwGetJoystickAxes(connected_joystick_id, &total_axes);
- // Read whatever buttons actually exist
+#if GLFW_VERSION_MAJOR > 3 || (GLFW_VERSION_MAJOR == 3 && GLFW_VERSION_MINOR >= 3)
+ int total_hats;
+ const unsigned char *hats = glfwGetJoystickHats(connected_joystick_id, &total_hats);
+#endif
+
+ // Handle directional inputs
+ status->bLeft = axes[0] < axis_neutrals[0] - DEADZONE;
+ status->bRight = axes[0] > axis_neutrals[0] + DEADZONE;
+ status->bUp = axes[1] < axis_neutrals[1] - DEADZONE;
+ status->bDown = axes[1] > axis_neutrals[1] + DEADZONE;
+
+ // Handle button inputs
+ unsigned int buttons_done = 0;
+
+ // Start with the joystick buttons
for (int i = 0; i < total_buttons; ++i)
- status->bButton[i] = buttons[i] == GLFW_PRESS;
+ {
+ status->bButton[buttons_done] = buttons[i] == GLFW_PRESS;
- // Blank the buttons that do not
- for (int i = total_buttons; i < 32; ++i)
+ if (++buttons_done >= button_limit)
+ break;
+ }
+
+ // Then the joystick axes
+ for (int i = 0; i < total_axes; ++i)
+ {
+ status->bButton[buttons_done] = axes[i] < axis_neutrals[i] - DEADZONE;
+
+ if (++buttons_done >= button_limit)
+ break;
+
+ status->bButton[buttons_done] = axes[i] > axis_neutrals[i] + DEADZONE;
+
+ if (++buttons_done >= button_limit)
+ break;
+ }
+
+#if GLFW_VERSION_MAJOR > 3 || (GLFW_VERSION_MAJOR == 3 && GLFW_VERSION_MINOR >= 3)
+ // Then the joystick hats
+ for (int i = 0; i < total_axes; ++i)
+ {
+ status->bButton[buttons_done] = hats[i] & GLFW_HAT_UP;
+
+ if (++buttons_done >= button_limit)
+ break;
+
+ status->bButton[buttons_done] = hats[i] & GLFW_HAT_RIGHT;
+
+ if (++buttons_done >= button_limit)
+ break;
+
+ status->bButton[buttons_done] = hats[i] & GLFW_HAT_DOWN;
+
+ if (++buttons_done >= button_limit)
+ break;
+
+ status->bButton[buttons_done] = hats[i] & GLFW_HAT_LEFT;
+
+ if (++buttons_done >= button_limit)
+ break;
+ }
+#endif
+
+ // Blank any remaining buttons
+ for (size_t i = buttons_done; i < button_limit; ++i)
status->bButton[i] = FALSE;
return TRUE;
@@ -117,8 +176,6 @@
{
if (!joystick_connected)
return FALSE;
-
- // The code that would normally run here has been moved to JoystickCallback, to better-support hotplugging
return TRUE;
}
--- a/src/Backends/GLFW3/Misc.cpp
+++ b/src/Backends/GLFW3/Misc.cpp
@@ -20,13 +20,13 @@
#define DO_KEY(GLFW_KEY, BACKEND_KEY) \
case GLFW_KEY: \
- backend_keyboard_state[BACKEND_KEY] = action == GLFW_PRESS; \
+ keyboard_state[BACKEND_KEY] = action == GLFW_PRESS; \
break;
BOOL bActive = TRUE;
-BOOL backend_keyboard_state[BACKEND_KEYBOARD_TOTAL];
-BOOL backend_previous_keyboard_state[BACKEND_KEYBOARD_TOTAL];
+static BOOL keyboard_state[BACKEND_KEYBOARD_TOTAL];
+
static GLFWcursor* cursor;
static void KeyCallback(GLFWwindow *window, int key, int scancode, int action, int mods)
@@ -264,8 +264,6 @@
return FALSE;
}
- memcpy(backend_previous_keyboard_state, backend_keyboard_state, sizeof(backend_keyboard_state));
-
glfwPollEvents();
while (!bActive)
@@ -272,6 +270,11 @@
glfwWaitEvents();
return TRUE;
+}
+
+void Backend_GetKeyboardState(BOOL *out_keyboard_state)
+{
+ memcpy(out_keyboard_state, keyboard_state, sizeof(keyboard_state));
}
void Backend_ShowMessageBox(const char *title, const char *message)
--- a/src/Backends/Misc.h
+++ b/src/Backends/Misc.h
@@ -84,8 +84,6 @@
};
extern BOOL bActive;
-extern BOOL backend_keyboard_state[BACKEND_KEYBOARD_TOTAL];
-extern BOOL backend_previous_keyboard_state[BACKEND_KEYBOARD_TOTAL];
void Backend_Init(void);
void Backend_Deinit(void);
@@ -96,6 +94,7 @@
void Backend_SetCursor(const unsigned char *rgb_pixels, unsigned int width, unsigned int height);
void PlaybackBackend_EnableDragAndDrop(void);
BOOL Backend_SystemTask(void);
+void Backend_GetKeyboardState(BOOL *keyboard_state);
void Backend_ShowMessageBox(const char *title, const char *message);
unsigned long Backend_GetTicks(void);
void Backend_Delay(unsigned int ticks);
--- a/src/Backends/SDL2/Controller.cpp
+++ b/src/Backends/SDL2/Controller.cpp
@@ -11,9 +11,9 @@
#define DEADZONE 10000;
static SDL_Joystick *joystick;
-static int joystick_neutral_x;
-static int joystick_neutral_y;
+static Sint16 *axis_neutrals;
+
BOOL ControllerBackend_Init(void)
{
SDL_InitSubSystem(SDL_INIT_JOYSTICK);
@@ -37,26 +37,77 @@
if (joystick == NULL)
return FALSE;
- // Read axis
+ const size_t button_limit = sizeof(status->bButton) / sizeof(status->bButton[0]);
+
+ // Handle directional inputs
const Sint16 joystick_x = SDL_JoystickGetAxis(joystick, 0);
const Sint16 joystick_y = SDL_JoystickGetAxis(joystick, 1);
- status->bLeft = joystick_x < joystick_neutral_x - DEADZONE;
- status->bRight = joystick_x > joystick_neutral_x + DEADZONE;
- status->bUp = joystick_y < joystick_neutral_y - DEADZONE;
- status->bDown = joystick_y > joystick_neutral_y + DEADZONE;
+ status->bLeft = joystick_x < axis_neutrals[0] - DEADZONE;
+ status->bRight = joystick_x > axis_neutrals[0] + DEADZONE;
+ status->bUp = joystick_y < axis_neutrals[1] - DEADZONE;
+ status->bDown = joystick_y > axis_neutrals[1] + DEADZONE;
- // The original `Input.cpp` assumed there were 32 buttons (because of DirectInput's `DIJOYSTATE` struct)
- int numButtons = SDL_JoystickNumButtons(joystick);
- if (numButtons > 32)
- numButtons = 32;
+ // Handle button inputs
+ int total_buttons = SDL_JoystickNumButtons(joystick);
+ int total_axes = SDL_JoystickNumAxes(joystick);
+ int total_hats = SDL_JoystickNumHats(joystick);
- // Read whatever buttons actually exist
- for (int i = 0; i < numButtons; ++i)
- status->bButton[i] = SDL_JoystickGetButton(joystick, i);
+ unsigned int buttons_done = 0;
- // Blank the buttons that do not
- for (int i = numButtons; i < 32; ++i)
+ // Start with the joystick buttons
+ for (int i = 0; i < total_buttons; ++i)
+ {
+ status->bButton[buttons_done] = SDL_JoystickGetButton(joystick, i);
+
+ if (++buttons_done >= button_limit)
+ break;
+ }
+
+ // Then the joystick axes
+ for (int i = 0; i < total_axes; ++i)
+ {
+ Sint16 axis = SDL_JoystickGetAxis(joystick, i);
+
+ status->bButton[buttons_done] = axis < axis_neutrals[i] - DEADZONE;
+
+ if (++buttons_done >= button_limit)
+ break;
+
+ status->bButton[buttons_done] = axis > axis_neutrals[i] + DEADZONE;
+
+ if (++buttons_done >= button_limit)
+ break;
+ }
+
+ // Then the joystick hats
+ for (int i = 0; i < total_axes; ++i)
+ {
+ Uint8 hat = SDL_JoystickGetHat(joystick, i);
+
+ status->bButton[buttons_done] = hat == SDL_HAT_UP || hat == SDL_HAT_LEFTUP || hat == SDL_HAT_RIGHTUP;
+
+ if (++buttons_done >= button_limit)
+ break;
+
+ status->bButton[buttons_done] = hat == SDL_HAT_RIGHT || hat == SDL_HAT_RIGHTUP || hat == SDL_HAT_RIGHTDOWN;
+
+ if (++buttons_done >= button_limit)
+ break;
+
+ status->bButton[buttons_done] = hat == SDL_HAT_DOWN || hat == SDL_HAT_LEFTDOWN || hat == SDL_HAT_RIGHTDOWN;
+
+ if (++buttons_done >= button_limit)
+ break;
+
+ status->bButton[buttons_done] = hat == SDL_HAT_LEFT || hat == SDL_HAT_LEFTUP || hat == SDL_HAT_LEFTDOWN;
+
+ if (++buttons_done >= button_limit)
+ break;
+ }
+
+ // Blank any remaining buttons
+ for (size_t i = buttons_done; i < button_limit; ++i)
status->bButton[i] = FALSE;
return TRUE;
@@ -67,8 +118,6 @@
if (joystick == NULL)
return FALSE;
- // The code that would normally run here has been moved to JoystickCallback, to better-support hotplugging
-
return TRUE;
}
@@ -82,11 +131,24 @@
if (joystick != NULL)
{
- printf("Joystick #%d selected\n", joystick_id);
+ int total_axes = SDL_JoystickNumAxes(joystick);
+ int total_buttons = SDL_JoystickNumButtons(joystick);
- // Reset default stick positions (this is performed in ResetJoystickStatus in vanilla Cave Story
- joystick_neutral_x = SDL_JoystickGetAxis(joystick, 0);
- joystick_neutral_y = SDL_JoystickGetAxis(joystick, 1);
+ if (total_axes >= 2 && total_buttons >= 6)
+ {
+ printf("Joystick #%d selected\n", joystick_id);
+
+ // Set up neutral axes
+ axis_neutrals = (Sint16*)malloc(sizeof(Sint16) * total_axes);
+
+ for (int i = 0; i < total_axes; ++i)
+ axis_neutrals[i] = SDL_JoystickGetAxis(joystick, i);
+ }
+ else
+ {
+ SDL_JoystickClose(joystick);
+ joystick = NULL;
+ }
}
}
}
@@ -96,6 +158,9 @@
if (joystick_id == SDL_JoystickInstanceID(joystick))
{
printf("Joystick #%d disconnected\n", joystick_id);
+ SDL_JoystickClose(joystick);
joystick = NULL;
+
+ free(axis_neutrals);
}
}
--- a/src/Backends/SDL2/Misc.cpp
+++ b/src/Backends/SDL2/Misc.cpp
@@ -18,13 +18,13 @@
#define DO_KEY(SDL_KEY, BACKEND_KEY) \
case SDL_KEY: \
- backend_keyboard_state[BACKEND_KEY] = event.key.type == SDL_KEYDOWN; \
+ keyboard_state[BACKEND_KEY] = event.key.type == SDL_KEYDOWN; \
break;
BOOL bActive = TRUE;
-BOOL backend_keyboard_state[BACKEND_KEYBOARD_TOTAL];
-BOOL backend_previous_keyboard_state[BACKEND_KEYBOARD_TOTAL];
+static BOOL keyboard_state[BACKEND_KEYBOARD_TOTAL];
+
static SDL_Surface *cursor_surface;
static SDL_Cursor *cursor;
@@ -97,8 +97,6 @@
BOOL Backend_SystemTask(void)
{
- memcpy(backend_previous_keyboard_state, backend_keyboard_state, sizeof(backend_keyboard_state));
-
while (SDL_PollEvent(NULL) || !bActive)
{
SDL_Event event;
@@ -238,6 +236,11 @@
}
return TRUE;
+}
+
+void Backend_GetKeyboardState(BOOL *out_keyboard_state)
+{
+ memcpy(out_keyboard_state, keyboard_state, sizeof(keyboard_state));
}
void Backend_ShowMessageBox(const char *title, const char *message)
--- a/src/Input.h
+++ b/src/Input.h
@@ -8,7 +8,7 @@
BOOL bRight;
BOOL bUp;
BOOL bDown;
- BOOL bButton[32];
+ BOOL bButton[32]; // The original `Input.cpp` assumed there were 32 buttons (because of DirectInput's `DIJOYSTATE` struct)
};
void ReleaseDirectInput(void);
--- a/src/Main.cpp
+++ b/src/Main.cpp
@@ -277,13 +277,16 @@
size_t window_icon_resource_size;
const unsigned char *window_icon_resource_data = FindResource("ICON_MINI", "ICON", &window_icon_resource_size);
- unsigned int window_icon_width, window_icon_height;
- unsigned char *window_icon_rgb_pixels = DecodeBitmap(window_icon_resource_data, window_icon_resource_size, &window_icon_width, &window_icon_height);
-
- if (window_icon_rgb_pixels != NULL)
+ if (window_icon_resource_data != NULL)
{
- Backend_SetWindowIcon(window_icon_rgb_pixels, window_icon_width, window_icon_height);
- FreeBitmap(window_icon_rgb_pixels);
+ unsigned int window_icon_width, window_icon_height;
+ unsigned char *window_icon_rgb_pixels = DecodeBitmap(window_icon_resource_data, window_icon_resource_size, &window_icon_width, &window_icon_height);
+
+ if (window_icon_rgb_pixels != NULL)
+ {
+ Backend_SetWindowIcon(window_icon_rgb_pixels, window_icon_width, window_icon_height);
+ FreeBitmap(window_icon_rgb_pixels);
+ }
}
#endif
@@ -291,13 +294,16 @@
size_t cursor_resource_size;
const unsigned char *cursor_resource_data = FindResource("CURSOR_NORMAL", "CURSOR", &cursor_resource_size);
- unsigned int cursor_width, cursor_height;
- unsigned char *cursor_rgb_pixels = DecodeBitmap(cursor_resource_data, cursor_resource_size, &cursor_width, &cursor_height);
-
- if (cursor_rgb_pixels != NULL)
+ if (cursor_resource_data != NULL)
{
- Backend_SetCursor(cursor_rgb_pixels, cursor_width, cursor_height);
- FreeBitmap(cursor_rgb_pixels);
+ unsigned int cursor_width, cursor_height;
+ unsigned char *cursor_rgb_pixels = DecodeBitmap(cursor_resource_data, cursor_resource_size, &cursor_width, &cursor_height);
+
+ if (cursor_rgb_pixels != NULL)
+ {
+ Backend_SetCursor(cursor_rgb_pixels, cursor_width, cursor_height);
+ FreeBitmap(cursor_rgb_pixels);
+ }
}
if (IsKeyFile("fps"))
@@ -379,12 +385,17 @@
BOOL SystemTask(void)
{
+ static BOOL previous_keyboard_state[BACKEND_KEYBOARD_TOTAL];
+
if (!Backend_SystemTask())
return FALSE;
+ BOOL keyboard_state[BACKEND_KEYBOARD_TOTAL];
+ Backend_GetKeyboardState(keyboard_state);
+
for (unsigned int i = 0; i < BACKEND_KEYBOARD_TOTAL; ++i)
{
- if ((backend_keyboard_state[i] ^ backend_previous_keyboard_state[i]) & backend_keyboard_state[i])
+ if (keyboard_state[i] && !previous_keyboard_state[i])
{
switch (i)
{
@@ -470,7 +481,7 @@
break;
}
}
- else if ((backend_keyboard_state[i] ^ backend_previous_keyboard_state[i]) & backend_previous_keyboard_state[i])
+ else if (!keyboard_state[i] && previous_keyboard_state[i])
{
switch (i)
{
@@ -554,6 +565,8 @@
}
}
+ memcpy(previous_keyboard_state, keyboard_state, sizeof(keyboard_state));
+
// Run joystick code
if (gbUseJoystick)
JoystickProc();
@@ -569,7 +582,7 @@
if (!GetJoystickStatus(&status))
return;
- gKey &= (KEY_ESCAPE | KEY_F2 | KEY_F1);
+ gKey &= (KEY_ESCAPE | KEY_F1 | KEY_F2);
// Set movement buttons
if (status.bLeft)