diff --git a/auxmos.dll b/auxmos.dll
index fc227f62c9..52e01b515a 100644
Binary files a/auxmos.dll and b/auxmos.dll differ
diff --git a/auxmos.pdb b/auxmos.pdb
index a2db349011..0f5a534a12 100644
Binary files a/auxmos.pdb and b/auxmos.pdb differ
diff --git a/code/controllers/configuration/entries/connections.dm b/code/controllers/configuration/entries/connections.dm
index acdf588e5a..6ec41590f2 100644
--- a/code/controllers/configuration/entries/connections.dm
+++ b/code/controllers/configuration/entries/connections.dm
@@ -1,6 +1,12 @@
/datum/config_entry/flag/panic_bunker // prevents people the server hasn't seen before from connecting
-/datum/config_entry/number/panic_bunker_living // living time in minutes that a player needs to pass the panic bunker
+/datum/config_entry/number/panic_bunker_living // living time in minutes that a player needs to pass the panic bunker. they pass **if they are above this amount**
+ config_entry_value = 0 // default: <= 0 meaning any playtime works. -1 to disable criteria.
+ integer = TRUE
+
+/datum/config_entry/number/panic_bunker_living_vpn
+ config_entry_value = 0 // default: <= 0 meaning anytime works. -1 to disable criteria.
+ integer = TRUE
/datum/config_entry/string/panic_bunker_message
config_entry_value = "Sorry but the server is currently not accepting connections from never before seen players."
diff --git a/code/controllers/subsystem/fail2topic.dm b/code/controllers/subsystem/fail2topic.dm
index 78c4807c3f..fc18d7dabe 100644
--- a/code/controllers/subsystem/fail2topic.dm
+++ b/code/controllers/subsystem/fail2topic.dm
@@ -33,7 +33,7 @@ SUBSYSTEM_DEF(fail2topic)
while(i <= length(rate_limiting))
var/ip = rate_limiting[i]
var/last_attempt = rate_limiting[ip]
- if(world.time - last_attempt > rate_limit)
+ if(REALTIMEOFDAY - last_attempt > rate_limit)
rate_limiting -= ip
fail_counts -= ip
else //if we remove that, and the next element is in its place. check that instead of incrementing.
@@ -58,12 +58,12 @@ SUBSYSTEM_DEF(fail2topic)
if (active_bans[ip])
return TRUE
- rate_limiting[ip] = world.time
+ rate_limiting[ip] = REALTIMEOFDAY
if (isnull(last_attempt))
return FALSE
- if (world.time - last_attempt > rate_limit)
+ if (REALTIMEOFDAY - last_attempt > rate_limit)
fail_counts -= ip
return FALSE
else
@@ -83,7 +83,7 @@ SUBSYSTEM_DEF(fail2topic)
if (!enabled)
return
- active_bans[ip] = world.time
+ active_bans[ip] = REALTIMEOFDAY
fail_counts -= ip
rate_limiting -= ip
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index 0d6b1231fc..53f64c57d2 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -131,7 +131,6 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb
//Tooltip vars
var/force_string //string form of an item's force. Edit this var only to set a custom force string
var/last_force_string_check = 0
- var/tip_timer
var/trigger_guard = TRIGGER_GUARD_NONE
@@ -451,7 +450,8 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb
A.Remove(user)
if(item_flags & DROPDEL)
qdel(src)
- item_flags &= ~IN_INVENTORY
+ DISABLE_BITFIELD(item_flags, IN_INVENTORY)
+ DISABLE_BITFIELD(item_flags, IN_STORAGE)
SEND_SIGNAL(src, COMSIG_ITEM_DROPPED,user)
remove_outline()
// if(!silent)
@@ -529,6 +529,8 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb
if(item_action_slot_check(slot, user, A)) //some items only give their actions buttons when in a specific slot.
A.Grant(user)
item_flags |= IN_INVENTORY
+ if(CHECK_BITFIELD(item_flags, IN_STORAGE)) // Left storage item but somehow has the bitfield active still.
+ DISABLE_BITFIELD(item_flags, IN_STORAGE)
// if(!initial)
// if(equip_sound && (slot_flags & slot))
// playsound(src, equip_sound, EQUIP_SOUND_VOLUME, TRUE, ignore_walls = FALSE)
@@ -885,10 +887,9 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb
/obj/item/MouseEntered(location, control, params)
SEND_SIGNAL(src, COMSIG_ITEM_MOUSE_ENTER, location, control, params)
- if((item_flags & IN_INVENTORY || item_flags & IN_STORAGE) && usr.client.prefs.enable_tips && !QDELETED(src))
- var/timedelay = usr.client.prefs.tip_delay/100
- var/user = usr
- tip_timer = addtimer(CALLBACK(src, .proc/openTip, location, control, params, user), timedelay, TIMER_STOPPABLE)//timer takes delay in deciseconds, but the pref is in milliseconds. dividing by 100 converts it.
+ if((item_flags & IN_INVENTORY || item_flags & IN_STORAGE) && usr?.client.prefs.enable_tips && !QDELETED(src))
+ var/timedelay = max(usr.client.prefs.tip_delay * 0.01, 0.01) // I heard multiplying is faster, also runtimes from very low/negative numbers
+ usr.client.tip_timer = addtimer(CALLBACK(src, .proc/openTip, location, control, params, usr), timedelay, TIMER_STOPPABLE)//timer takes delay in deciseconds, but the pref is in milliseconds. dividing by 100 converts it.
var/mob/living/L = usr
if(istype(L) && (L.incapacitated() || (current_equipped_slot in L.check_obscured_slots()) || !L.canUnEquip(src)))
apply_outline(_size = 3)
@@ -901,7 +902,6 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb
/obj/item/MouseExited(location,control,params)
SEND_SIGNAL(src, COMSIG_ITEM_MOUSE_EXIT, location, control, params)
- deltimer(tip_timer)//delete any in-progress timer if the mouse is moved off the item before it finishes
closeToolTip(usr)
remove_outline()
diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm
index 4e57a2f717..fe1fb03426 100644
--- a/code/game/objects/structures/crates_lockers/closets.dm
+++ b/code/game/objects/structures/crates_lockers/closets.dm
@@ -540,18 +540,19 @@
if (!(. & EMP_PROTECT_CONTENTS))
for(var/obj/O in src)
O.emp_act(severity)
- if(secure && !broken && !(. & EMP_PROTECT_SELF))
- if(prob(50 / severity))
- locked = !locked
- update_icon()
- if(prob(20 / severity) && !opened)
- if(!locked)
- open()
- else
- req_access = list()
- req_access += pick(get_all_accesses())
- if(!QDELETED(lockerelectronics))
- lockerelectronics.accesses = req_access
+ if(!secure || broken)
+ return
+ if(prob(severity/2))
+ locked = !locked
+ update_icon()
+ if(prob(severity/5) && !opened)
+ if(!locked)
+ open()
+ else
+ req_access = list()
+ req_access += pick(get_all_accesses())
+ if(!QDELETED(lockerelectronics))
+ lockerelectronics.accesses = req_access
/obj/structure/closet/contents_explosion(severity, target)
for(var/atom/A in contents)
diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm
index 99f24107c7..950b2b5b25 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -187,3 +187,6 @@
var/list/block_parry_hinted = list()
/// moused over objects, currently capped at 7. this is awful, and should be replaced with a component to track it using signals for parrying at some point.
var/list/moused_over_objects = list()
+
+ /// AFK tracking
+ var/last_activity = 0
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 73902f18fb..b9589d02bc 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -83,6 +83,8 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
log_href("[src] (usr:[usr]\[[COORD(usr)]\]) : [hsrc ? "[hsrc] " : ""][href]")
return
+ last_activity = world.time
+
//Logs all hrefs
log_href("[src] (usr:[usr]\[[COORD(usr)]\]) : [hsrc ? "[hsrc] " : ""][href]")
@@ -222,6 +224,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
///////////
/client/New(TopicData)
+ last_activity = world.time
world.SetConfig("APP/admin", ckey, "role=admin")
var/tdata = TopicData //save this for later use
TopicData = null //Prevent calls to client.Topic from connect
@@ -578,9 +581,10 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
//If we aren't an admin, and the flag is set
if(CONFIG_GET(flag/panic_bunker) && !holder && !GLOB.deadmins[ckey] && !(ckey in GLOB.bunker_passthrough))
var/living_recs = CONFIG_GET(number/panic_bunker_living)
+ var/vpn_living_recs = CONFIG_GET(number/panic_bunker_living_vpn)
//Relies on pref existing, but this proc is only called after that occurs, so we're fine.
var/minutes = get_exp_living(pure_numeric = TRUE)
- if(minutes <= living_recs) // && !CONFIG_GET(flag/panic_bunker_interview)
+ if((minutes <= living_recs) || (IsVPN() && (minutes < vpn_living_recs)))
var/reject_message = "Failed Login: [key] - Account attempting to connect during panic bunker, but they do not have the required living time [minutes]/[living_recs]"
log_access(reject_message)
message_admins("[reject_message]")
@@ -850,6 +854,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
/client/Click(atom/object, atom/location, control, params, ignore_spam = FALSE, extra_info)
if(last_click > world.time - world.tick_lag)
return
+ last_activity = world.time
last_click = world.time
var/ab = FALSE
var/list/L = params2list(params)
@@ -922,6 +927,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
//checks if a client is afk
//3000 frames = 5 minutes
/client/proc/is_afk(duration = CONFIG_GET(number/inactivity_period))
+ var/inactivity = world.time - last_activity
if(inactivity > duration)
return inactivity
return FALSE
diff --git a/code/modules/client/client_vpn_detect.dm b/code/modules/client/client_vpn_detect.dm
new file mode 100644
index 0000000000..bd35a65ef2
--- /dev/null
+++ b/code/modules/client/client_vpn_detect.dm
@@ -0,0 +1,3 @@
+/client/proc/IsVPN()
+ var/datum/ipintel/res = get_ip_intel(address)
+ return res.intel >= CONFIG_GET(number/ipintel_rating_bad)
diff --git a/code/modules/client/verbs/etips.dm b/code/modules/client/verbs/etips.dm
index 27434cddc4..d4ebc899ae 100644
--- a/code/modules/client/verbs/etips.dm
+++ b/code/modules/client/verbs/etips.dm
@@ -15,6 +15,6 @@
var/indelay = stripped_input(usr, "Enter the tooltip delay in milliseconds (default: 500)", "Enter tooltip delay", "", 10)
indelay = text2num(indelay)
if(usr)//is this what you mean?
- prefs.tip_delay = indelay
+ prefs.tip_delay = max(indelay, 0.01)
prefs.save_preferences()
to_chat(usr, "Tooltip delay set to [indelay] milliseconds.")
diff --git a/code/modules/events/pirates.dm b/code/modules/events/pirates.dm
index 5708573887..d8496818eb 100644
--- a/code/modules/events/pirates.dm
+++ b/code/modules/events/pirates.dm
@@ -24,7 +24,7 @@
var/pirate_type = PIRATES_ROGUES //pick(PIRATES_ROGUES, PIRATES_SILVERSCALES, PIRATES_DUTCHMAN)
var/datum/comm_message/threat_msg = new
var/payoff = 0
- var/payoff_min = 10000
+ var/payoff_min = 1000
var/ship_template
var/ship_name = "Space Privateers Association"
var/initial_send_time = world.time
diff --git a/code/modules/keybindings/bindings_client.dm b/code/modules/keybindings/bindings_client.dm
index 3d12f89cfe..7c0c6216a5 100644
--- a/code/modules/keybindings/bindings_client.dm
+++ b/code/modules/keybindings/bindings_client.dm
@@ -6,6 +6,7 @@
set hidden = TRUE
client_keysend_amount += 1
+ last_activity = world.time
var/cache = client_keysend_amount
@@ -89,6 +90,7 @@
set hidden = TRUE
keys_held -= _key
+ last_activity = world.time
var/movement = movement_keys[_key]
if(!(next_move_dir_add & movement))
next_move_dir_sub |= movement
diff --git a/code/modules/language/language_holder.dm b/code/modules/language/language_holder.dm
index 219d8dc184..2ad1bb762b 100644
--- a/code/modules/language/language_holder.dm
+++ b/code/modules/language/language_holder.dm
@@ -312,12 +312,14 @@ Key procs
/datum/language/machine = list(LANGUAGE_ATOM),
/datum/language/draconic = list(LANGUAGE_ATOM),
/datum/language/slime = list(LANGUAGE_ATOM),
- /datum/language/dwarf = list(LANGUAGE_ATOM))
+ /datum/language/dwarf = list(LANGUAGE_ATOM),
+ /datum/language/neokanji = list(LANGUAGE_ATOM))
spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
/datum/language/machine = list(LANGUAGE_ATOM),
/datum/language/draconic = list(LANGUAGE_ATOM),
/datum/language/slime = list(LANGUAGE_ATOM),
- /datum/language/dwarf = list(LANGUAGE_ATOM))
+ /datum/language/dwarf = list(LANGUAGE_ATOM),
+ /datum/language/neokanji = list(LANGUAGE_ATOM))
/datum/language_holder/venus
understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
diff --git a/code/modules/language/neokanji.dm b/code/modules/language/neokanji.dm
new file mode 100644
index 0000000000..bfd7face5b
--- /dev/null
+++ b/code/modules/language/neokanji.dm
@@ -0,0 +1,25 @@
+/datum/language/neokanji
+ name = "Neo-Kanji"
+ desc = "A bastardized mixture of many old Earth asian dialects. Famously known as the official language of the spider clan."
+ speech_verb = "proclaims"
+ ask_verb = "queries"
+ exclaim_verb = "declares"
+ whisper_verb = "hushes"
+ key = "k"
+ space_chance = 40
+ default_priority = 94
+ flags = TONGUELESS_SPEECH
+ chooseable_roundstart = TRUE
+ syllables = list("ka", "ki", "ku", "ke", "ko", "ta",
+ "chi", "tsu", "te", "to", "sa", "shi",
+ "su", "se", "so", "na","ni","nu","ne",
+ "no","n","ha","hi","fu","he","ho","ma",
+ "mi","mu","me","mo","ya","yu","yo","ra",
+ "ri","ru","re","ro","wa","wo", "an", "ang",
+ "ao", "ba", "bai", "ban", "bang", "bao",
+ "bei", "beng", "chuai", "xing", "xong", "zhao",
+ "zhong", "xil", "ping", "dang", "guang", "guan",
+ "jing", "jiao", "kung", "fu", "lo", "wang",
+ "liu", "ling", "mang", "nong", "peng", "qiong",
+ "san","tiao", "wan","xiong", "men")
+ icon_state = "neokanji"
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 4c03d8cd1c..55afe78e2e 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -1055,10 +1055,10 @@
return
if(!HAS_TRAIT(src, TRAIT_IGNOREDAMAGESLOWDOWN)) //if we want to ignore slowdown from damage, but not from equipment
var/scaling = maxHealth / 100
- var/health_deficiency = max(((maxHealth / scaling) - (health / scaling)), (getStaminaLoss()*0.75))
+ var/health_deficiency = max(((maxHealth / scaling) - (health / scaling)), max(0, getStaminaLoss() - 39))
if(health_deficiency >= 40)
- add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/damage_slowdown, TRUE, (health_deficiency - 15) / 75)
- add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/damage_slowdown_flying, TRUE, (health_deficiency - 15) / 25)
+ add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/damage_slowdown, TRUE, health_deficiency / 75)
+ add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/damage_slowdown_flying, TRUE, health_deficiency / 25)
else
remove_movespeed_modifier(/datum/movespeed_modifier/damage_slowdown)
remove_movespeed_modifier(/datum/movespeed_modifier/damage_slowdown_flying)
diff --git a/code/modules/mob/say.dm b/code/modules/mob/say.dm
index e92886571c..d332aa56c2 100644
--- a/code/modules/mob/say.dm
+++ b/code/modules/mob/say.dm
@@ -4,6 +4,7 @@
set name = "say_indicator"
set hidden = TRUE
set category = "IC"
+ client?.last_activity = world.time
display_typing_indicator()
var/message = tgui_input_text(usr, null, "say")
// If they don't type anything just drop the message.
@@ -21,12 +22,16 @@
to_chat(usr, "Speech is currently admin-disabled.")
return
clear_typing_indicator() // clear it immediately!
+
+ client?.last_activity = world.time
+
say(message)
/mob/verb/me_typing_indicator()
set name = "me_indicator"
set hidden = TRUE
set category = "IC"
+ client?.last_activity = world.time
display_typing_indicator()
var/message = tgui_input_message(usr, null, "me")
// If they don't type anything just drop the message.
@@ -52,6 +57,8 @@
message = trim(copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN))
clear_typing_indicator() // clear it immediately!
+ client?.last_activity = world.time
+
usr.emote("me",1,message,TRUE)
/mob/say_mod(input, message_mode)
@@ -73,6 +80,7 @@
return lowertext(copytext_char(input, 1, customsayverb))
/mob/proc/whisper_keybind()
+ client?.last_activity = world.time
var/message = tgui_input_text(src, "", "whisper")
if(!length(message))
return
@@ -89,6 +97,7 @@
whisper(message)
/mob/proc/whisper(message, datum/language/language=null)
+ client?.last_activity = world.time
say(message, language) //only living mobs actually whisper, everything else just talks
/mob/proc/say_dead(var/message)
@@ -132,6 +141,7 @@
message = emoji_parse(message)
var/rendered = "DEAD: [name][alt_name] "
log_talk(message, LOG_SAY, tag="DEAD")
+ client?.last_activity = world.time
deadchat_broadcast(rendered, follow_target = src, speaker_key = key)
/mob/proc/check_emote(message)
diff --git a/code/modules/ninja/outfit.dm b/code/modules/ninja/outfit.dm
index a6faa9a3d5..ca740d9ad7 100644
--- a/code/modules/ninja/outfit.dm
+++ b/code/modules/ninja/outfit.dm
@@ -21,3 +21,6 @@
var/obj/item/clothing/suit/space/space_ninja/S = H.wear_suit
if(istype(H.belt, belt))
S.energyKatana = H.belt
+ H.grant_language(/datum/language/neokanji)
+ var/datum/language_holder/LH = H.get_language_holder()
+ LH.selected_language = /datum/language/neokanji
diff --git a/code/modules/projectiles/guns/ballistic/shotgun.dm b/code/modules/projectiles/guns/ballistic/shotgun.dm
index eea16ac636..6ba2f49791 100644
--- a/code/modules/projectiles/guns/ballistic/shotgun.dm
+++ b/code/modules/projectiles/guns/ballistic/shotgun.dm
@@ -246,15 +246,15 @@
mag_type = /obj/item/ammo_box/magazine/internal/shot/com
w_class = WEIGHT_CLASS_NORMAL
var/stock = FALSE
+ var/extend_sound = 'sound/weapons/batonextend.ogg'
recoil = 5
spread = 2
/obj/item/gun/ballistic/shotgun/automatic/combat/compact/AltClick(mob/living/user)
- . = ..()
- if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, ismonkey(user)) || item_flags && IN_STORAGE)
+ if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, ismonkey(user)) || item_flags & IN_STORAGE)
return
toggle_stock(user)
- return TRUE
+ . = ..()
/obj/item/gun/ballistic/shotgun/automatic/combat/compact/examine(mob/user)
. = ..()
@@ -272,6 +272,7 @@
to_chat(user, "You fold the stock.")
recoil = 5
spread = 2
+ playsound(src.loc, extend_sound, 50, 1)
update_icon()
/obj/item/gun/ballistic/shotgun/automatic/combat/compact/update_icon_state()
diff --git a/code/modules/surgery/organs/tongue.dm b/code/modules/surgery/organs/tongue.dm
index 7cba3d358a..833fd5bc94 100644
--- a/code/modules/surgery/organs/tongue.dm
+++ b/code/modules/surgery/organs/tongue.dm
@@ -26,6 +26,7 @@
/datum/language/vampiric,
/datum/language/dwarf,
/datum/language/signlanguage,
+ /datum/language/neokanji,
))
healing_factor = STANDARD_ORGAN_HEALING*5 //Fast!!
decay_factor = STANDARD_ORGAN_DECAY/2
@@ -253,7 +254,8 @@
/datum/language/beachbum,
/datum/language/aphasia,
/datum/language/sylvan,
- /datum/language/voltaic
+ /datum/language/voltaic,
+ /datum/language/neokanji,
))
/obj/item/organ/tongue/ethereal/Initialize(mapload)
diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm
index a0c39258b9..c739d69d0b 100644
--- a/code/modules/tgui/tgui.dm
+++ b/code/modules/tgui/tgui.dm
@@ -300,6 +300,7 @@
process_status()
if(src_object.ui_act(act_type, payload, src, state))
SStgui.update_uis(src_object)
+ usr?.client?.last_activity = world.time
return FALSE
switch(type)
if("ready")
diff --git a/code/modules/tooltip/tooltip.dm b/code/modules/tooltip/tooltip.dm
index 65e44cfbc6..725eca89db 100644
--- a/code/modules/tooltip/tooltip.dm
+++ b/code/modules/tooltip/tooltip.dm
@@ -119,15 +119,18 @@ Notes:
//Includes sanity checks.
/proc/closeToolTip(mob/user)
if(istype(user))
- if(user.client && user.client.tooltips)
- user.client.tooltips.hide()
+ if(user.client)
+ var/client/client = user.client
+ if(client.tooltips)
+ client.tooltips.hide()
+ deltimer(client.tip_timer) //delete any in-progress timer if the mouse is moved off the item before it finishes
+ client.tip_timer = null
/**
* # `get_tooltip_data()`
*
* If set, will return a list for the tooltip (that will also be put together in a `Join()`)
- * However, if returning `null`, falls back to default behavior, which is `examine(src)`, and it will definitely include
- * images since it is the default behavior
+ * However, if returning `null`, the tooltip will not be shown as #14942 changed it.
*
* Though no tooltips will be created for atoms that have `tooltips = FALSE`
*/
@@ -137,11 +140,12 @@ Notes:
/atom/movable/MouseEntered(location, control, params)
. = ..()
if(tooltips)
- if(!QDELETED(src) && usr.client.prefs.enable_tips)
+ if(!QDELETED(src) && usr?.client.prefs.enable_tips)
var/list/tooltip_data = get_tooltip_data()
if(length(tooltip_data))
var/examine_data = tooltip_data.Join("
")
- openToolTip(usr, src, params, title = name, content = examine_data)
+ var/timedelay = max(usr.client.prefs.tip_delay * 0.01, 0.01) // I heard multiplying is faster, also runtimes from very low/negative numbers
+ usr.client.tip_timer = addtimer(CALLBACK(GLOBAL_PROC, .proc/openToolTip, usr, src, params, name, examine_data), timedelay, TIMER_STOPPABLE)
/atom/movable/MouseExited(location, control, params)
. = ..()
@@ -150,3 +154,7 @@ Notes:
/client/MouseDown(object, location, control, params)
closeToolTip(usr)
. = ..()
+
+/client
+ /// Timers are now handled by clients, not by doing a mess on the item and multiple people overwriting a single timer on the object, have fun.
+ var/tip_timer = null
diff --git a/config/entries/connections.txt b/config/entries/connections.txt
index 6351a77fcf..41df00bc40 100644
--- a/config/entries/connections.txt
+++ b/config/entries/connections.txt
@@ -2,9 +2,12 @@
## Requires database
#PANIC_BUNKER
-## If a player connects during a bunker with less then or this amount of living time (Minutes), we deny the connection
+## If a player connects during a bunker with less then or this amount of living time (Minutes), we deny the connection. Set to -1 to disable criteria.
#PANIC_BUNKER_LIVING 90
+## The above but for VPN connections. Set to -1 to disable criteria. You can set the above to disabled and set this to only enforce bunker on VPNs.
+#PANIC_BUNKER_LIVING_VPN 120
+
## The message the Panic Bunker gives when someone is rejected by it
## %minutes% is replaced with PANIC_BUNKER_LIVING on runtime, remove it if you don't want this
#PANIC_BUNKER_MESSAGE Sorry, but the server is currently not accepting connections from players with less than %minutes% minutes of living time.
diff --git a/html/changelog.html b/html/changelog.html
index 515eeef06f..d85bd6b47a 100644
--- a/html/changelog.html
+++ b/html/changelog.html
@@ -508,33 +508,6 @@