diff --git a/code/__DEFINES/text.dm b/code/__DEFINES/text.dm index ffac885bf4c8..c303eebbcdc5 100644 --- a/code/__DEFINES/text.dm +++ b/code/__DEFINES/text.dm @@ -58,6 +58,12 @@ /// Removes everything enclose in < and > inclusive of the bracket, and limits the length of the message. #define STRIP_HTML_FULL(text, limit) (GLOB.html_tags.Replace(copytext(text, 1, limit), "")) +/** + * stuff like `copytext(input, length(input))` will trim the last character of the input, + * because DM does it so it copies until the char BEFORE the `end` arg, so we need to bump `end` by 1 in these cases. + */ +#define PREVENT_CHARACTER_TRIM_LOSS(integer) (integer + 1) + /// Folder directory for strings #define STRING_DIRECTORY "strings" diff --git a/code/datums/components/jetpack.dm b/code/datums/components/jetpack.dm index bf3e5fee5cd4..3aa6ab973154 100644 --- a/code/datums/components/jetpack.dm +++ b/code/datums/components/jetpack.dm @@ -114,7 +114,7 @@ return if(user.throwing)//You don't must use jet if you thrown return - if(length(user.client.keys_held & user.client.movement_keys))//You use jet when press keys. yes. + if(user.client.intended_direction)//You use jet when press keys. yes. thrust() /datum/component/jetpack/proc/pre_move_react(mob/user) diff --git a/code/datums/components/scope.dm b/code/datums/components/scope.dm index 2e10f7663a73..5886c73e8154 100644 --- a/code/datums/components/scope.dm +++ b/code/datums/components/scope.dm @@ -35,7 +35,7 @@ stop_zooming(user_mob) return tracker.calculate_params() - if(!length(user_client.keys_held & user_client.movement_keys)) + if(!user_client.intended_direction) user_mob.face_atom(tracker.given_turf) animate(user_client, world.tick_lag, pixel_x = tracker.given_x, pixel_y = tracker.given_y) diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index 77930876b060..142d12382f0d 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -248,6 +248,9 @@ var/list/keys_held = list() /// A buffer for combinations such of modifiers + keys (ex: CtrlD, AltE, ShiftT). Format: `"key"` -> `"combo"` (ex: `"D"` -> `"CtrlD"`) var/list/key_combos_held = list() + /// The direction we WANT to move, based off our keybinds + /// Will be udpated to be the actual direction later on + var/intended_direction = NONE /* ** These next two vars are to apply movement for keypresses and releases made while move delayed. ** Because discarding that input makes the game less responsive. diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 47334aef6e0a..856c31229624 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -1176,6 +1176,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( winset(src, "default-[REF(key)]", "parent=default;name=[key];command=[msay]") else winset(src, "default-[REF(key)]", "parent=default;name=[key];command=") + calculate_move_dir() /client/proc/change_view(new_size) if (isnull(new_size)) diff --git a/code/modules/keybindings/bindings_atom.dm b/code/modules/keybindings/bindings_atom.dm index 6dadcd5768ee..e99e3714c6b6 100644 --- a/code/modules/keybindings/bindings_atom.dm +++ b/code/modules/keybindings/bindings_atom.dm @@ -2,12 +2,21 @@ // Only way to do that is to tie the behavior into the focus's keyLoop(). /atom/movable/keyLoop(client/user) - var/movement_dir = NONE - for(var/_key in user?.keys_held) - movement_dir = movement_dir | user.movement_keys[_key] - if(user?.next_move_dir_add) - movement_dir |= user.next_move_dir_add - if(user?.next_move_dir_sub) + // Clients don't go null randomly. They do go null unexpectedly though, when they're poked in particular ways + // keyLoop is called by a for loop over mobs. We're guarenteed that all the mobs have clients at the START + // But the move of one mob might poke the client of another, so we do this + if(!user) + return FALSE + var/movement_dir = user.intended_direction | user.next_move_dir_add + // If we're not movin anywhere, we aren't movin anywhere + // Safe because nothing adds to movement_dir after this moment + if(!movement_dir) + // No input == our removal would have done nothing + // So we can safely forget about it + user.next_move_dir_sub = NONE + return FALSE + + if(user.next_move_dir_sub) movement_dir &= ~user.next_move_dir_sub // Sanity checks in case you hold left and right and up to make sure you only go up if((movement_dir & NORTH) && (movement_dir & SOUTH)) @@ -15,14 +24,21 @@ if((movement_dir & EAST) && (movement_dir & WEST)) movement_dir &= ~(EAST|WEST) - if(user && movement_dir) //If we're not moving, don't compensate, as byond will auto-fill dir otherwise + if(user.dir != NORTH && movement_dir) //If we're not moving, don't compensate, as byond will auto-fill dir otherwise movement_dir = turn(movement_dir, -dir2angle(user.dir)) //By doing this we ensure that our input direction is offset by the client (camera) direction //turn without moving while using the movement lock key, unless something wants to ignore it and move anyway - if(user?.movement_locked && !(SEND_SIGNAL(src, COMSIG_MOVABLE_KEYBIND_FACE_DIR, movement_dir) & COMSIG_IGNORE_MOVEMENT_LOCK)) + if(user.movement_locked && !(SEND_SIGNAL(src, COMSIG_MOVABLE_KEYBIND_FACE_DIR, movement_dir) & COMSIG_IGNORE_MOVEMENT_LOCK)) keybind_face_direction(movement_dir) - else - user?.Move(get_step(src, movement_dir), movement_dir) + // Null check cause of the signal above + else if(user) + user.Move(get_step(src, movement_dir), movement_dir) return !!movement_dir //true if there was actually any player input return FALSE + +/client/proc/calculate_move_dir() + var/movement_dir = NONE + for(var/_key in keys_held) + movement_dir |= movement_keys[_key] + intended_direction = movement_dir diff --git a/code/modules/keybindings/bindings_client.dm b/code/modules/keybindings/bindings_client.dm index e4d002142827..bef16d57fec6 100644 --- a/code/modules/keybindings/bindings_client.dm +++ b/code/modules/keybindings/bindings_client.dm @@ -47,9 +47,10 @@ //the time a key was pressed isn't actually used anywhere (as of 2019-9-10) but this allows easier access usage/checking keys_held[_key] = world.time - if(!movement_locked) - var/movement = movement_keys[_key] - if(!(next_move_dir_sub & movement)) + var/movement = movement_keys[_key] + if(movement) + calculate_move_dir() + if(!movement_locked && !(next_move_dir_sub & movement)) next_move_dir_add |= movement // Client-level keybindings are ones anyone should be able to do at any time @@ -94,9 +95,10 @@ keys_held -= _key - if(!movement_locked) - var/movement = movement_keys[_key] - if(!(next_move_dir_add & movement)) + var/movement = movement_keys[_key] + if(movement) + calculate_move_dir() + if(!movement_locked && !(next_move_dir_add & movement)) next_move_dir_sub |= movement // We don't do full key for release, because for mod keys you diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm index 2f08bbc8ab2f..b7a0d779c8ab 100644 --- a/code/modules/mob/mob_movement.dm +++ b/code/modules/mob/mob_movement.dm @@ -66,8 +66,8 @@ /client/Move(new_loc, direct) if(world.time < move_delay) //do not move anything ahead of this check please return FALSE - next_move_dir_add = 0 - next_move_dir_sub = 0 + next_move_dir_add = NONE + next_move_dir_sub = NONE var/old_move_delay = move_delay move_delay = world.time + world.tick_lag //this is here because Move() can now be called mutiple times per tick if(!direct || !new_loc) diff --git a/code/modules/tgui_input/alert.dm b/code/modules/tgui_input/alert.dm index 4ff7ab81ab15..4749ef278725 100644 --- a/code/modules/tgui_input/alert.dm +++ b/code/modules/tgui_input/alert.dm @@ -10,7 +10,7 @@ * * timeout - The timeout of the alert, after which the modal will close and qdel itself. Set to zero for no timeout. * * autofocus - The bool that controls if this alert should grab window focus. */ -/proc/tgui_alert(mob/user, message = "", title, list/buttons = list("Ok"), timeout = 0, autofocus = TRUE) +/proc/tgui_alert(mob/user, message = "", title, list/buttons = list("Ok"), timeout = 0, autofocus = TRUE, ui_state = GLOB.always_state) if (!user) user = usr if (!istype(user)) @@ -18,7 +18,11 @@ var/client/client = user user = client.mob else - return + return null + + if(isnull(user.client)) + return null + // A gentle nudge - you should not be using TGUI alert for anything other than a simple message. if(length(buttons) > 3) log_tgui(user, "Error: TGUI Alert initiated with too many buttons. Use a list.", "TguiAlert") @@ -29,7 +33,7 @@ return alert(user, message, title, buttons[1], buttons[2]) if(length(buttons) == 3) return alert(user, message, title, buttons[1], buttons[2], buttons[3]) - var/datum/tgui_alert/alert = new(user, message, title, buttons, timeout, autofocus) + var/datum/tgui_alert/alert = new(user, message, title, buttons, timeout, autofocus, ui_state) alert.ui_interact(user) alert.wait() if (alert) @@ -59,12 +63,15 @@ var/autofocus /// Boolean field describing if the tgui_alert was closed by the user. var/closed + /// The TGUI UI state that will be returned in ui_state(). Default: always_state + var/datum/ui_state/state -/datum/tgui_alert/New(mob/user, message, title, list/buttons, timeout, autofocus) +/datum/tgui_alert/New(mob/user, message, title, list/buttons, timeout, autofocus, ui_state) src.autofocus = autofocus src.buttons = buttons.Copy() src.message = message src.title = title + src.state = ui_state if (timeout) src.timeout = timeout start_time = world.time @@ -72,6 +79,7 @@ /datum/tgui_alert/Destroy(force) SStgui.close_uis(src) + state = null QDEL_NULL(buttons) return ..() @@ -94,7 +102,7 @@ closed = TRUE /datum/tgui_alert/ui_state(mob/user) - return GLOB.always_state + return state /datum/tgui_alert/ui_static_data(mob/user) var/list/data = list() diff --git a/code/modules/tgui_input/checkboxes.dm b/code/modules/tgui_input/checkboxes.dm index 87b9ac3ddd00..53b264038dc2 100644 --- a/code/modules/tgui_input/checkboxes.dm +++ b/code/modules/tgui_input/checkboxes.dm @@ -10,20 +10,24 @@ * max_checked - The maximum number of checkboxes that can be checked (optional) * timeout - The timeout for the input (optional) */ -/proc/tgui_input_checkboxes(mob/user, message, title = "Select", list/items, min_checked = 1, max_checked = 50, timeout = 0) +/proc/tgui_input_checkboxes(mob/user, message, title = "Select", list/items, min_checked = 1, max_checked = 50, timeout = 0, ui_state = GLOB.always_state) if (!user) user = usr if(!length(items)) - return + return null if (!istype(user)) if (istype(user, /client)) var/client/client = user user = client.mob else - return + return null + + if(isnull(user.client)) + return null + if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) return input(user, message, title) as null|anything in items - var/datum/tgui_checkbox_input/input = new(user, message, title, items, min_checked, max_checked, timeout) + var/datum/tgui_checkbox_input/input = new(user, message, title, items, min_checked, max_checked, timeout, ui_state) input.ui_interact(user) input.wait() if (input) @@ -50,13 +54,16 @@ var/min_checked /// Maximum number of checkboxes that can be checked var/max_checked + /// The TGUI UI state that will be returned in ui_state(). Default: always_state + var/datum/ui_state/state -/datum/tgui_checkbox_input/New(mob/user, message, title, list/items, min_checked, max_checked, timeout) +/datum/tgui_checkbox_input/New(mob/user, message, title, list/items, min_checked, max_checked, timeout, ui_state) src.title = title src.message = message src.items = items.Copy() src.min_checked = min_checked src.max_checked = max_checked + src.state = ui_state if (timeout) src.timeout = timeout @@ -65,6 +72,7 @@ /datum/tgui_checkbox_input/Destroy(force) SStgui.close_uis(src) + state = null QDEL_NULL(items) return ..() @@ -84,7 +92,7 @@ closed = TRUE /datum/tgui_checkbox_input/ui_state(mob/user) - return GLOB.always_state + return state /datum/tgui_checkbox_input/ui_data(mob/user) var/list/data = list() diff --git a/code/modules/tgui_input/list.dm b/code/modules/tgui_input/list.dm index 9fefd4f2fa83..268b1a196293 100644 --- a/code/modules/tgui_input/list.dm +++ b/code/modules/tgui_input/list.dm @@ -10,21 +10,25 @@ * * default - If an option is already preselected on the UI. Current values, etc. * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. */ -/proc/tgui_input_list(mob/user, message, title = "Select", list/items, default, timeout = 0) +/proc/tgui_input_list(mob/user, message, title = "Select", list/items, default, timeout = 0, ui_state = GLOB.always_state) if (!user) user = usr if(!length(items)) - return + return null if (!istype(user)) if (istype(user, /client)) var/client/client = user user = client.mob else - return + return null + + if(isnull(user.client)) + return null + /// Client does NOT have tgui_input on: Returns regular input if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) return input(user, message, title, default) as null|anything in items - var/datum/tgui_list_input/input = new(user, message, title, items, default, timeout) + var/datum/tgui_list_input/input = new(user, message, title, items, default, timeout, ui_state) input.ui_interact(user) input.wait() if (input) @@ -56,13 +60,16 @@ var/timeout /// Boolean field describing if the tgui_list_input was closed by the user. var/closed + /// The TGUI UI state that will be returned in ui_state(). Default: always_state + var/datum/ui_state/state -/datum/tgui_list_input/New(mob/user, message, title, list/items, default, timeout) +/datum/tgui_list_input/New(mob/user, message, title, list/items, default, timeout, ui_state) src.title = title src.message = message src.items = list() src.items_map = list() src.default = default + src.state = ui_state var/list/repeat_items = list() // Gets rid of illegal characters var/static/regex/whitelistedWords = regex(@{"([^\u0020-\u8000]+)"}) @@ -81,6 +88,7 @@ /datum/tgui_list_input/Destroy(force) SStgui.close_uis(src) + state = null QDEL_NULL(items) return ..() @@ -103,7 +111,7 @@ closed = TRUE /datum/tgui_list_input/ui_state(mob/user) - return GLOB.always_state + return state /datum/tgui_list_input/ui_static_data(mob/user) var/list/data = list() diff --git a/code/modules/tgui_input/number.dm b/code/modules/tgui_input/number.dm index 10413c3f175f..68998acb0331 100644 --- a/code/modules/tgui_input/number.dm +++ b/code/modules/tgui_input/number.dm @@ -15,7 +15,7 @@ * * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout. * * round_value - whether the inputted number is rounded down into an integer. */ -/proc/tgui_input_number(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, timeout = 0, round_value = TRUE) +/proc/tgui_input_number(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, timeout = 0, round_value = TRUE, ui_state = GLOB.always_state) if (!user) user = usr if (!istype(user)) @@ -23,12 +23,16 @@ var/client/client = user user = client.mob else - return + return null + + if (isnull(user.client)) + return null + // Client does NOT have tgui_input on: Returns regular input if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) var/input_number = input(user, message, title, default) as null|num return clamp(round_value ? round(input_number) : input_number, min_value, max_value) - var/datum/tgui_input_number/number_input = new(user, message, title, default, max_value, min_value, timeout, round_value) + var/datum/tgui_input_number/number_input = new(user, message, title, default, max_value, min_value, timeout, round_value, ui_state) number_input.ui_interact(user) number_input.wait() if (number_input) @@ -62,14 +66,17 @@ var/timeout /// The title of the TGUI window var/title + /// The TGUI UI state that will be returned in ui_state(). Default: always_state + var/datum/ui_state/state -/datum/tgui_input_number/New(mob/user, message, title, default, max_value, min_value, timeout, round_value) +/datum/tgui_input_number/New(mob/user, message, title, default, max_value, min_value, timeout, round_value, ui_state) src.default = default src.max_value = max_value src.message = message src.min_value = min_value src.title = title src.round_value = round_value + src.state = ui_state if (timeout) src.timeout = timeout start_time = world.time @@ -87,6 +94,7 @@ /datum/tgui_input_number/Destroy(force) SStgui.close_uis(src) + state = null return ..() /** @@ -108,7 +116,7 @@ closed = TRUE /datum/tgui_input_number/ui_state(mob/user) - return GLOB.always_state + return state /datum/tgui_input_number/ui_static_data(mob/user) var/list/data = list() diff --git a/code/modules/tgui_input/text.dm b/code/modules/tgui_input/text.dm index b0474e4b14e9..66b61d88ac34 100644 --- a/code/modules/tgui_input/text.dm +++ b/code/modules/tgui_input/text.dm @@ -10,12 +10,12 @@ * * message - The content of the text input, shown in the body of the TGUI window. * * title - The title of the text input modal, shown on the top of the TGUI window. * * default - The default (or current) value, shown as a placeholder. - * * max_length - Specifies a max length for input. MAX_MESSAGE_LEN is default (1024) + * * max_length - Specifies a max length for input. By default is infinity. * * multiline - Bool that determines if the input box is much larger. Good for large messages, laws, etc. * * encode - Toggling this determines if input is filtered via html_encode. Setting this to FALSE gives raw input. * * timeout - The timeout of the textbox, after which the modal will close and qdel itself. Set to zero for no timeout. */ -/proc/tgui_input_text(mob/user, message = "", title = "Text Input", default, max_length = MAX_MESSAGE_LEN, multiline = FALSE, encode = TRUE, timeout = 0) +/proc/tgui_input_text(mob/user, message = "", title = "Text Input", default, max_length, multiline = FALSE, encode = TRUE, timeout = 0, ui_state = GLOB.always_state) if (!user) user = usr if (!istype(user)) @@ -23,20 +23,24 @@ var/client/client = user user = client.mob else - return + return null + + if(isnull(user.client)) + return null + // Client does NOT have tgui_input on: Returns regular input if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) if(encode) if(multiline) - return stripped_multiline_input(user, message, title, default, max_length) + return stripped_multiline_input(user, message, title, default, PREVENT_CHARACTER_TRIM_LOSS(max_length)) else - return stripped_input(user, message, title, default, max_length) + return stripped_input(user, message, title, default, PREVENT_CHARACTER_TRIM_LOSS(max_length)) else if(multiline) return input(user, message, title, default) as message|null else return input(user, message, title, default) as text|null - var/datum/tgui_input_text/text_input = new(user, message, title, default, max_length, multiline, encode, timeout) + var/datum/tgui_input_text/text_input = new(user, message, title, default, max_length, multiline, encode, timeout, ui_state) text_input.ui_interact(user) text_input.wait() if (text_input) @@ -70,14 +74,17 @@ var/timeout /// The title of the TGUI window var/title + /// The TGUI UI state that will be returned in ui_state(). Default: always_state + var/datum/ui_state/state -/datum/tgui_input_text/New(mob/user, message, title, default, max_length, multiline, encode, timeout) +/datum/tgui_input_text/New(mob/user, message, title, default, max_length, multiline, encode, timeout, ui_state) src.default = default src.encode = encode src.max_length = max_length src.message = message src.multiline = multiline src.title = title + src.state = ui_state if (timeout) src.timeout = timeout start_time = world.time @@ -85,6 +92,7 @@ /datum/tgui_input_text/Destroy(force) SStgui.close_uis(src) + state = null return ..() /** @@ -106,7 +114,7 @@ closed = TRUE /datum/tgui_input_text/ui_state(mob/user) - return GLOB.always_state + return state /datum/tgui_input_text/ui_static_data(mob/user) var/list/data = list() @@ -154,4 +162,4 @@ /datum/tgui_input_text/proc/set_entry(entry) if(!isnull(entry)) var/converted_entry = encode ? html_encode(entry) : entry - src.entry = trim(converted_entry, max_length) + src.entry = max_length ? trim(converted_entry, PREVENT_CHARACTER_TRIM_LOSS(max_length)) : converted_entry diff --git a/tgui/packages/tgui/interfaces/TextInputModal.tsx b/tgui/packages/tgui/interfaces/TextInputModal.tsx index 392ce99fe92d..80302ea58367 100644 --- a/tgui/packages/tgui/interfaces/TextInputModal.tsx +++ b/tgui/packages/tgui/interfaces/TextInputModal.tsx @@ -78,7 +78,7 @@ export const TextInputModal = (props) => { diff --git a/tgui/public/tgui.html b/tgui/public/tgui.html index 8937a9443bc4..de9a7e9b26d9 100644 --- a/tgui/public/tgui.html +++ b/tgui/public/tgui.html @@ -346,6 +346,18 @@ if (!sync) { node.media = 'only x'; } + var removeNodeAndRetry = function () { + node.parentNode.removeChild(node); + node = null; + retry(); + } + // 516: Chromium won't call onload() if there is a 404 error + // Legacy IE doesn't use onerror, so we retain that + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#stylesheet_load_events + node.onerror = function () { + node.onerror = null; + removeNodeAndRetry(); + } node.onload = function () { node.onload = null; if (isStyleSheetLoaded(node, url)) { @@ -353,10 +365,7 @@ node.media = 'all'; return; } - // Try again - node.parentNode.removeChild(node); - node = null; - retry(); + removeNodeAndRetry(); }; injectNode(node); return;