use emscripten HTML5 API for keyboard events instead of old JS library

This commit is contained in:
Toad King 2018-01-13 17:54:47 -06:00
parent 161d758b33
commit 1e212b4248
5 changed files with 283 additions and 302 deletions

View File

@ -42,7 +42,6 @@ LIBS := -s USE_SDL=2 -s USE_ZLIB=1
LDFLAGS := -L. --no-heap-copy -s USE_ZLIB=1 -s TOTAL_MEMORY=$(MEMORY) \
-s EXPORTED_FUNCTIONS="['_main', '_malloc', '_cmd_savefiles', '_cmd_save_state', '_cmd_load_state', '_cmd_take_screenshot']" \
--js-library emscripten/library_rwebaudio.js \
--js-library emscripten/library_rwebinput.js \
--js-library emscripten/library_rwebcam.js
ifneq ($(PTHREAD), 0)
LDFLAGS += -s USE_PTHREADS=$(PTHREAD) -s PTHREAD_POOL_SIZE=2

View File

@ -1,143 +0,0 @@
//"use strict";
var LibraryRWebInput = {
$RI__deps: ['$Browser'],
$RI: {
temp: null,
contexts: [],
stateX: 0,
stateY: 0,
currentX: 0,
currentY: 0,
canvasEventHandler: function(event) {
switch (event.type) {
case 'mouseup':
case 'mousedown':
var value;
var offset;
if (event.button === 0) offset = 40;
else if (event.button === 2) offset = 41;
else break;
if (event.type === 'mouseup') value = 0;
else value = 1;
for (var i = 0; i < RI.contexts.length; i++) {
{{{ makeSetValue('RI.contexts[i].state', 'offset', 'value', 'i8') }}};
}
break;
}
},
eventHandler: function(event) {
var i;
switch (event.type) {
case 'mousemove':
var x = 0;
var y = 0;
var newX = event['clientX'] - Module.canvas.offsetLeft;
var newY = event['clientY'] - Module.canvas.offsetTop;
if (newX < 0) {
newX = 0;
x = -Module.canvas.offsetWidth;
} else if (newX > Module.canvas.offsetWidth) {
newX = Module.canvas.offsetWidth;
x = Module.canvas.offsetWidth;
} else {
x = newX - RI.currentX;
}
if (newY < 0) {
newY = 0;
y = -Module.canvas.offsetHeight;
} else if (newY > Module.canvas.offsetHeight) {
newY = Module.canvas.offsetHeight;
y = Module.canvas.offsetHeight;
} else {
y = newY - RI.currentY;
}
RI.currentX = newX;
RI.currentY = newY;
for (i = 0; i < RI.contexts.length; i++) {
{{{ makeSetValue('RI.contexts[i].state', '32', 'x', 'i32') }}};
{{{ makeSetValue('RI.contexts[i].state', '36', 'y', 'i32') }}};
}
break;
case 'keyup':
case 'keydown':
var key = event.keyCode;
var offset = key >> 3;
var bit = 1 << (key & 7);
if (offset >= 32) throw 'key code error! bad code: ' + key;
for (i = 0; i < RI.contexts.length; i++) {
var value = {{{ makeGetValue('RI.contexts[i].state', 'offset', 'i8') }}};
if (event.type === 'keyup') value &= ~bit;
else value |= bit;
{{{ makeSetValue('RI.contexts[i].state', 'offset', 'value', 'i8') }}};
}
event.preventDefault();
break;
case 'blur':
case 'visibilitychange':
for (i = 0; i < RI.contexts.length; i++) {
_memset(RI.contexts[i].state, 0, 42);
}
break;
}
}
},
RWebInputInit: function() {
if (RI.contexts.length === 0) {
document.addEventListener('keyup', RI.eventHandler, false);
document.addEventListener('keydown', RI.eventHandler, false);
document.addEventListener('mousemove', RI.eventHandler, false);
Module.canvas.addEventListener('mouseup', RI.canvasEventHandler, false);
Module.canvas.addEventListener('mousedown', RI.canvasEventHandler, false);
document.addEventListener('blur', RI.eventHandler, false);
document.addEventListener('onvisbilitychange', RI.eventHandler, false);
}
if (RI.temp === null) RI.temp = _malloc(42);
var s = _malloc(42);
_memset(s, 0, 42);
RI.contexts.push({
state: s
});
return RI.contexts.length;
},
RWebInputPoll: function(context) {
context -= 1;
var state = RI.contexts[context].state;
_memcpy(RI.temp, state, 42);
// reset mouse movements
{{{ makeSetValue('RI.contexts[context].state', '32', '0', 'i32') }}};
{{{ makeSetValue('RI.contexts[context].state', '36', '0', 'i32') }}};
return RI.temp;
},
RWebInputDestroy: function (context) {
if (context === RI.contexts.length) {
RI.contexts.pop();
if (RI.contexts.length === 0) {
document.removeEventListener('keyup', RI.eventHandler, false);
document.removeEventListener('keydown', RI.eventHandler, false);
document.removeEventListener('mousemove', RI.eventHandler, false);
Module.canvas.removeEventListener('mouseup', RI.canvasEventHandler, false);
Module.canvas.removeEventListener('mousedown', RI.canvasEventHandler, false);
document.removeEventListener('blur', RI.eventHandler, false);
document.removeEventListener('onvisbilitychange', RI.eventHandler, false);
}
}
}
};
autoAddDeps(LibraryRWebInput, '$RI');
mergeInto(LibraryManager.library, LibraryRWebInput);

View File

@ -19,50 +19,295 @@
#include <string.h>
#include <boolean.h>
#include <retro_assert.h>
#include <retro_miscellaneous.h>
#include <encodings/crc32.h>
#include <encodings/utf.h>
#include <emscripten/html5.h>
#include "../input_driver.h"
#include "../input_keymaps.h"
#include "../../tasks/tasks_internal.h"
#include "../../configuration.h"
#include "../../verbosity.h"
typedef struct rwebinput_state
typedef struct rwebinput_key_to_code_map_entry
{
uint8_t keys[32];
const char *key;
enum retro_key rk;
} rwebinput_key_to_code_map_entry_t;
typedef struct
{
bool keys[RETROK_LAST];
} rwebinput_key_states_t;
typedef struct rwebinput_input
{
rwebinput_key_states_t keyboard;
int mouse_x;
int mouse_y;
char mouse_l;
char mouse_r;
bool blocked;
} rwebinput_state_t;
int RWebInputInit(void);
rwebinput_state_t *RWebInputPoll(int context);
void RWebInputDestroy(int context);
typedef struct rwebinput_input
{
rwebinput_state_t state;
int context;
} rwebinput_input_t;
/* KeyboardEvent.keyCode has been deprecated for a while and doesn't have
* separate left/right modifer codes, so we have to map string labels from
* KeyboardEvent.code to retro keys */
static const rwebinput_key_to_code_map_entry_t rwebinput_key_to_code_map[] =
{
{ "KeyA", RETROK_a },
{ "KeyB", RETROK_b },
{ "KeyC", RETROK_c },
{ "KeyD", RETROK_d },
{ "KeyE", RETROK_e },
{ "KeyF", RETROK_f },
{ "KeyG", RETROK_g },
{ "KeyH", RETROK_h },
{ "KeyI", RETROK_i },
{ "KeyJ", RETROK_j },
{ "KeyK", RETROK_k },
{ "KeyL", RETROK_l },
{ "KeyM", RETROK_m },
{ "KeyN", RETROK_n },
{ "KeyO", RETROK_o },
{ "KeyP", RETROK_p },
{ "KeyQ", RETROK_q },
{ "KeyR", RETROK_r },
{ "KeyS", RETROK_s },
{ "KeyT", RETROK_t },
{ "KeyU", RETROK_u },
{ "KeyV", RETROK_v },
{ "KeyW", RETROK_w },
{ "KeyX", RETROK_x },
{ "KeyY", RETROK_y },
{ "KeyZ", RETROK_z },
{ "ArrowLeft", RETROK_LEFT },
{ "ArrowRight", RETROK_RIGHT },
{ "ArrowUp", RETROK_UP },
{ "ArrowDown", RETROK_DOWN },
{ "Enter", RETROK_RETURN },
{ "NumpadEnter", RETROK_KP_ENTER },
{ "Tab", RETROK_TAB },
{ "Insert", RETROK_INSERT },
{ "Delete", RETROK_DELETE },
{ "End", RETROK_END },
{ "Home", RETROK_HOME },
{ "ShiftRight", RETROK_RSHIFT },
{ "ShiftLeft", RETROK_LSHIFT },
{ "ControlLeft", RETROK_LCTRL },
{ "AltLeft", RETROK_LALT },
{ "Space", RETROK_SPACE },
{ "Escape", RETROK_ESCAPE },
{ "NumpadAdd", RETROK_KP_PLUS },
{ "NumpadSubtract", RETROK_KP_MINUS },
{ "F1", RETROK_F1 },
{ "F2", RETROK_F2 },
{ "F3", RETROK_F3 },
{ "F4", RETROK_F4 },
{ "F5", RETROK_F5 },
{ "F6", RETROK_F6 },
{ "F7", RETROK_F7 },
{ "F8", RETROK_F8 },
{ "F9", RETROK_F9 },
{ "F10", RETROK_F10 },
{ "F11", RETROK_F11 },
{ "F12", RETROK_F12 },
{ "Digit0", RETROK_0 },
{ "Digit1", RETROK_1 },
{ "Digit2", RETROK_2 },
{ "Digit3", RETROK_3 },
{ "Digit4", RETROK_4 },
{ "Digit5", RETROK_5 },
{ "Digit6", RETROK_6 },
{ "Digit7", RETROK_7 },
{ "Digit8", RETROK_8 },
{ "Digit9", RETROK_9 },
{ "PageUp", RETROK_PAGEUP },
{ "PageDown", RETROK_PAGEDOWN },
{ "Numpad0", RETROK_KP0 },
{ "Numpad1", RETROK_KP1 },
{ "Numpad2", RETROK_KP2 },
{ "Numpad3", RETROK_KP3 },
{ "Numpad4", RETROK_KP4 },
{ "Numpad5", RETROK_KP5 },
{ "Numpad6", RETROK_KP6 },
{ "Numpad7", RETROK_KP7 },
{ "Numpad8", RETROK_KP8 },
{ "Numpad9", RETROK_KP9 },
{ "Period", RETROK_PERIOD },
{ "CapsLock", RETROK_CAPSLOCK },
{ "NumLock", RETROK_NUMLOCK },
{ "Backspace", RETROK_BACKSPACE },
{ "NumpadMultiply", RETROK_KP_MULTIPLY },
{ "NumpadDivide", RETROK_KP_DIVIDE },
{ "PrintScreen", RETROK_PRINT },
{ "ScrollLock", RETROK_SCROLLOCK },
{ "Backquote", RETROK_BACKQUOTE },
{ "Pause", RETROK_PAUSE },
{ "Quote", RETROK_QUOTE },
{ "Comma", RETROK_COMMA },
{ "Minus", RETROK_MINUS },
{ "Slash", RETROK_SLASH },
{ "Semicolon", RETROK_SEMICOLON },
{ "Equal", RETROK_EQUALS },
{ "BracketLeft", RETROK_LEFTBRACKET },
{ "Backslash", RETROK_BACKSLASH },
{ "BracketRight", RETROK_RIGHTBRACKET },
{ "NumpadDecimal", RETROK_KP_PERIOD },
{ "NumpadEqual", RETROK_KP_EQUALS },
{ "ControlRight", RETROK_RCTRL },
{ "AltRight", RETROK_RALT },
{ "F13", RETROK_F13 },
{ "F14", RETROK_F14 },
{ "F15", RETROK_F15 },
{ "MetaRight", RETROK_RMETA },
{ "MetaLeft", RETROK_LMETA },
{ "Help", RETROK_HELP },
{ "ContextMenu", RETROK_MENU },
{ "Power", RETROK_POWER },
};
static bool g_initialized;
static rwebinput_key_states_t *g_keyboard;
/* to make the string labels for codes from JavaScript work, we convert them
* to CRC32 hashes for the LUT */
static void rwebinput_generate_lut(void)
{
int i;
struct rarch_key_map *key_map;
retro_assert(ARRAY_SIZE(rarch_key_map_rwebinput) ==
ARRAY_SIZE(rwebinput_key_to_code_map) + 1);
for (i = 0; i < ARRAY_SIZE(rwebinput_key_to_code_map); i++)
{
int j;
uint32_t crc;
const rwebinput_key_to_code_map_entry_t *key_to_code =
&rwebinput_key_to_code_map[i];
key_map = &rarch_key_map_rwebinput[i];
crc = encoding_crc32(0, (const uint8_t *)key_to_code->key,
strlen(key_to_code->key));
/* sanity check: make sure there's no collisions */
for (j = 0; j < i; j++)
{
retro_assert(rarch_key_map_rwebinput[j].sym != crc);
}
key_map->rk = key_to_code->rk;
key_map->sym = crc;
}
/* set terminating entry */
key_map = &rarch_key_map_rwebinput[ARRAY_SIZE(rarch_key_map_rwebinput) - 1];
key_map->rk = RETROK_UNKNOWN;
key_map->sym = 0;
}
static EM_BOOL rwebinput_input_cb(int event_type,
const EmscriptenKeyboardEvent *key_event, void *user_data)
{
uint32_t crc;
uint32_t keycode;
unsigned translated_keycode;
uint32_t character = 0;
uint16_t mod = 0;
bool keydown = event_type == EMSCRIPTEN_EVENT_KEYDOWN;
(void)user_data;
/* capture keypress events and silence them */
if (event_type == EMSCRIPTEN_EVENT_KEYPRESS)
return EM_TRUE;
/* a printable key: populate character field */
if (utf8len(key_event->key) == 1)
{
const char *key_ptr = &key_event->key[0];
character = utf8_walk(&key_ptr);
}
if (key_event->ctrlKey)
mod |= RETROKMOD_CTRL;
if (key_event->altKey)
mod |= RETROKMOD_ALT;
if (key_event->shiftKey)
mod |= RETROKMOD_SHIFT;
if (key_event->metaKey)
mod |= RETROKMOD_META;
keycode = encoding_crc32(0, (const uint8_t *)key_event->code,
strnlen(key_event->code, sizeof(key_event->code)));
translated_keycode = input_keymaps_translate_keysym_to_rk(keycode);
input_keyboard_event(keydown, translated_keycode, character, mod,
RETRO_DEVICE_KEYBOARD);
if (translated_keycode < RETROK_LAST)
{
g_keyboard->keys[translated_keycode] = keydown;
}
return EM_TRUE;
}
static void *rwebinput_input_init(const char *joypad_driver)
{
rwebinput_input_t *rwebinput = (rwebinput_input_t*)calloc(1, sizeof(*rwebinput));
if (!rwebinput)
rwebinput_input_t *rwebinput =
(rwebinput_input_t*)calloc(1, sizeof(*rwebinput));
g_keyboard =
(rwebinput_key_states_t*)calloc(1, sizeof(rwebinput_key_states_t));
if (!rwebinput || !g_keyboard)
goto error;
rwebinput->context = RWebInputInit();
if (!rwebinput->context)
goto error;
if (!g_initialized)
{
EMSCRIPTEN_RESULT r;
g_initialized = true;
rwebinput_generate_lut();
/* emscripten currently doesn't have an API to remove handlers, so make
* once and reuse it */
r = emscripten_set_keydown_callback("#document", NULL, false,
rwebinput_input_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS)
{
RARCH_ERR(
"[EMSCRIPTEN/INPUT] failed to create keydown callback: %d\n", r);
}
r = emscripten_set_keyup_callback("#document", NULL, false,
rwebinput_input_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS)
{
RARCH_ERR(
"[EMSCRIPTEN/INPUT] failed to create keydown callback: %d\n", r);
}
r = emscripten_set_keypress_callback("#document", NULL, false,
rwebinput_input_cb);
if (r != EMSCRIPTEN_RESULT_SUCCESS)
{
RARCH_ERR(
"[EMSCRIPTEN/INPUT] failed to create keypress callback: %d\n", r);
}
}
input_keymaps_init_keyboard_lut(rarch_key_map_rwebinput);
return rwebinput;
error:
if (rwebinput)
free(rwebinput);
free(g_keyboard);
free(rwebinput);
return NULL;
}
@ -75,10 +320,7 @@ static bool rwebinput_key_pressed(void *data, int key)
if (key >= RETROK_LAST)
return false;
sym = rarch_keysym_lut[(enum retro_key)key];
ret = rwebinput->state.keys[sym >> 3] & (1 << (sym & 7));
return ret;
return rwebinput->keyboard.keys[key];
}
static bool rwebinput_is_pressed(rwebinput_input_t *rwebinput,
@ -100,13 +342,13 @@ static int16_t rwebinput_mouse_state(rwebinput_input_t *rwebinput, unsigned id)
switch (id)
{
case RETRO_DEVICE_ID_MOUSE_X:
return (int16_t) rwebinput->state.mouse_x;
return (int16_t) rwebinput->mouse_x;
case RETRO_DEVICE_ID_MOUSE_Y:
return (int16_t) rwebinput->state.mouse_y;
return (int16_t) rwebinput->mouse_y;
case RETRO_DEVICE_ID_MOUSE_LEFT:
return rwebinput->state.mouse_l;
return rwebinput->mouse_l;
case RETRO_DEVICE_ID_MOUSE_RIGHT:
return rwebinput->state.mouse_r;
return rwebinput->mouse_r;
}
return 0;
@ -155,11 +397,12 @@ static void rwebinput_input_free(void *data)
{
rwebinput_input_t *rwebinput = (rwebinput_input_t*)data;
free(g_keyboard);
g_keyboard = NULL;
if (!rwebinput)
return;
RWebInputDestroy(rwebinput->context);
free(rwebinput);
}
@ -167,29 +410,8 @@ static void rwebinput_input_poll(void *data)
{
unsigned i;
rwebinput_input_t *rwebinput = (rwebinput_input_t*)data;
rwebinput_state_t *state = RWebInputPoll(rwebinput->context);
/* Get new keys. */
for (i = 0; i < 32; i++)
{
unsigned k;
uint8_t diff;
if (state->keys[i] == rwebinput->state.keys[i])
continue;
diff = state->keys[i] ^ rwebinput->state.keys[i];
for (k = 0; diff; diff >>= 1, k++)
{
if (diff & 1)
input_keyboard_event((state->keys[i] & (1 << k)) != 0,
input_keymaps_translate_keysym_to_rk(i * 8 + k), 0, 0,
RETRO_DEVICE_KEYBOARD);
}
}
memcpy(&rwebinput->state, state, sizeof(rwebinput->state));
memcpy(&rwebinput->keyboard, g_keyboard, sizeof(*g_keyboard));
}
static void rwebinput_grab_mouse(void *data, bool state)
@ -226,7 +448,7 @@ static bool rwebinput_keyboard_mapping_is_blocked(void *data)
rwebinput_input_t *rwebinput = (rwebinput_input_t*)data;
if (!rwebinput)
return false;
return rwebinput->state.blocked;
return rwebinput->blocked;
}
static void rwebinput_keyboard_mapping_set_block(void *data, bool value)
@ -234,7 +456,7 @@ static void rwebinput_keyboard_mapping_set_block(void *data, bool value)
rwebinput_input_t *rwebinput = (rwebinput_input_t*)data;
if (!rwebinput)
return;
rwebinput->state.blocked = value;
rwebinput->blocked = value;
}
input_driver_t input_rwebinput = {

View File

@ -579,109 +579,8 @@ const struct rarch_key_map rarch_key_map_dinput[] = {
#endif
#ifdef EMSCRIPTEN
const struct rarch_key_map rarch_key_map_rwebinput[] = {
{ 37, RETROK_LEFT },
{ 39, RETROK_RIGHT },
{ 38, RETROK_UP },
{ 40, RETROK_DOWN },
{ 13, RETROK_RETURN },
{ 9, RETROK_TAB },
{ 45, RETROK_INSERT },
{ 46, RETROK_DELETE },
{ 16, RETROK_RSHIFT },
{ 16, RETROK_LSHIFT },
{ 17, RETROK_LCTRL },
{ 35, RETROK_END },
{ 36, RETROK_HOME },
{ 34, RETROK_PAGEDOWN },
{ 33, RETROK_PAGEUP },
{ 18, RETROK_LALT },
{ 32, RETROK_SPACE },
{ 27, RETROK_ESCAPE },
{ 8, RETROK_BACKSPACE },
{ 13, RETROK_KP_ENTER },
{ 107, RETROK_KP_PLUS },
{ 109, RETROK_KP_MINUS },
{ 106, RETROK_KP_MULTIPLY },
{ 111, RETROK_KP_DIVIDE },
{ 192, RETROK_BACKQUOTE },
{ 19, RETROK_PAUSE },
{ 96, RETROK_KP0 },
{ 97, RETROK_KP1 },
{ 98, RETROK_KP2 },
{ 99, RETROK_KP3 },
{ 100, RETROK_KP4 },
{ 101, RETROK_KP5 },
{ 102, RETROK_KP6 },
{ 103, RETROK_KP7 },
{ 104, RETROK_KP8 },
{ 105, RETROK_KP9 },
{ 48, RETROK_0 },
{ 49, RETROK_1 },
{ 50, RETROK_2 },
{ 51, RETROK_3 },
{ 52, RETROK_4 },
{ 53, RETROK_5 },
{ 54, RETROK_6 },
{ 55, RETROK_7 },
{ 56, RETROK_8 },
{ 57, RETROK_9 },
{ 112, RETROK_F1 },
{ 113, RETROK_F2 },
{ 114, RETROK_F3 },
{ 115, RETROK_F4 },
{ 116, RETROK_F5 },
{ 117, RETROK_F6 },
{ 118, RETROK_F7 },
{ 119, RETROK_F8 },
{ 120, RETROK_F9 },
{ 121, RETROK_F10 },
{ 122, RETROK_F11 },
{ 123, RETROK_F12 },
{ 65, RETROK_a },
{ 66, RETROK_b },
{ 67, RETROK_c },
{ 68, RETROK_d },
{ 69, RETROK_e },
{ 70, RETROK_f },
{ 71, RETROK_g },
{ 72, RETROK_h },
{ 73, RETROK_i },
{ 74, RETROK_j },
{ 75, RETROK_k },
{ 76, RETROK_l },
{ 77, RETROK_m },
{ 78, RETROK_n },
{ 79, RETROK_o },
{ 80, RETROK_p },
{ 81, RETROK_q },
{ 82, RETROK_r },
{ 83, RETROK_s },
{ 84, RETROK_t },
{ 85, RETROK_u },
{ 86, RETROK_v },
{ 87, RETROK_w },
{ 88, RETROK_x },
{ 89, RETROK_y },
{ 90, RETROK_z },
{ 222, RETROK_QUOTE },
{ 188, RETROK_COMMA },
{ 173, RETROK_MINUS },
{ 191, RETROK_SLASH },
{ 59, RETROK_SEMICOLON },
{ 61, RETROK_EQUALS },
{ 219, RETROK_LEFTBRACKET },
{ 220, RETROK_BACKSLASH },
{ 221, RETROK_RIGHTBRACKET },
{ 188, RETROK_KP_PERIOD },
{ 17, RETROK_RCTRL },
{ 18, RETROK_RALT },
{ 190, RETROK_PERIOD },
{ 145, RETROK_SCROLLOCK },
{ 20, RETROK_CAPSLOCK },
{ 144, RETROK_NUMLOCK },
{ 0, RETROK_UNKNOWN },
};
/* this is generated at runtime, so it isn't constant */
struct rarch_key_map rarch_key_map_rwebinput[RARCH_KEY_MAP_RWEBINPUT_SIZE];
#endif
#ifdef WIIU

View File

@ -46,13 +46,18 @@ struct apple_key_name_map_entry
extern const struct apple_key_name_map_entry apple_key_name_map[];
#endif
#define RARCH_KEY_MAP_RWEBINPUT_SIZE 111
extern const struct input_key_map input_config_key_map[];
extern const struct rarch_key_map rarch_key_map_x11[];
extern const struct rarch_key_map rarch_key_map_sdl[];
extern const struct rarch_key_map rarch_key_map_sdl2[];
extern const struct rarch_key_map rarch_key_map_dinput[];
extern const struct rarch_key_map rarch_key_map_rwebinput[];
/* is generated at runtime so can't be const */
extern struct rarch_key_map rarch_key_map_rwebinput[RARCH_KEY_MAP_RWEBINPUT_SIZE];
extern const struct rarch_key_map rarch_key_map_linux[];
extern const struct rarch_key_map rarch_key_map_apple_hid[];
extern const struct rarch_key_map rarch_key_map_android[];
@ -96,4 +101,3 @@ extern enum retro_key rarch_keysym_lut[RETROK_LAST];
RETRO_END_DECLS
#endif