diff --git a/SQL/database_schema.sql b/SQL/database_schema.sql
index cfe8b134434..7cd5286854b 100644
--- a/SQL/database_schema.sql
+++ b/SQL/database_schema.sql
@@ -77,6 +77,56 @@ CREATE TABLE IF NOT EXISTS `%_PREFIX_%player` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+-- Playtime / JEXP --
+
+-- Role Time Table - Master --
+-- Stores total role time. --
+
+CREATE TABLE IF NOT EXISTS `%_PREFIX_%playtime` (
+ `player` INT(11) NOT NULL,
+ `roleid` VARCHAR(64) NOT NULL,
+ `minutes` INT UNSIGNED NOT NULL,
+ PRIMARY KEY(`player`, `roleid`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- Role Time - Logging --
+-- Stores changes in role time --
+CREATE TABLE IF NOT EXISTS `%_PREFIX_%playtime_log` (
+ `player` INT(11),
+ `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
+ `roleid` VARCHAR(64) NOT NULL,
+ `delta` INT(11) NOT NULL,
+ `datetime` TIMESTAMP NOT NULL DEFAULT NOW() ON UPDATE NOW(),
+ PRIMARY KEY (`id`),
+ KEY `player` (`player`),
+ KEY `roleid` (`roleid`),
+ KEY `datetime` (`datetime`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+DELIMITER $$
+CREATE TRIGGER `playtimeTlogupdate` AFTER UPDATE ON `%_PREFIX_%playtime` FOR EACH ROW BEGIN INSERT into `%_PREFIX_%playtime_log` (player, roleid, delta) VALUES (NEW.player, NEW.roleid, NEW.minutes-OLD.minutes);
+END
+$$
+CREATE TRIGGER `playtimeTloginsert` AFTER INSERT ON `%_PREFIX_%playtime` FOR EACH ROW BEGIN INSERT into `%_PREFIX_%playtime_log` (player, roleid, delta) VALUES (NEW.player, NEW.roleid, NEW.minutes);
+END
+$$
+CREATE TRIGGER `playtimeTlogdelete` AFTER DELETE ON `%_PREFIX_%playtime` FOR EACH ROW BEGIN INSERT into `%_PREFIX_%playtime_log` (player, roleid, delta) VALUES (OLD.player, OLD.roleid, 0-OLD.minutes);
+END
+$$
+DELIMITER ;
+
+-- Security - Ipintel --
+
+-- Ipintel Cache Table --
+-- Stores cache entries for IPIntel --
+-- IP is in INET_ATON. --
+CREATE TABLE IF NOT EXISTS `%_PREFIX_%ipintel` (
+ `ip` INT(10) unsigned NOT NULL,
+ `date` TIMESTAMP NOT NULL DEFAULT NOW() ON UPDATE NOW(),
+ `intel` double NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ip`),
+ KEY `idx_ipintel` (`ip`, `intel`, `date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
--
-- Table structure for table `round`
diff --git a/SQL/database_schema_prefixed.sql b/SQL/database_schema_prefixed.sql
index 372db7a5086..ae943cf666d 100644
--- a/SQL/database_schema_prefixed.sql
+++ b/SQL/database_schema_prefixed.sql
@@ -38,7 +38,7 @@ CREATE TABLE IF NOT EXISTS `rp_pictures` (
-- picture is picture hash in picture table --
CREATE TABLE IF NOT EXISTS `rp_photographs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
- `picture` char(40) NULL,
+ `picture` char(40) NOT NULL,
`created` datetime NOT NULL DEFAULT Now(),
`scene` MEDIUMTEXT null,
`desc` MEDIUMTEXT null,
@@ -77,6 +77,56 @@ CREATE TABLE IF NOT EXISTS `rp_player` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+-- Playtime / JEXP --
+
+-- Role Time Table - Master --
+-- Stores total role time. --
+
+CREATE TABLE IF NOT EXISTS `rp_playtime` (
+ `player` INT(11) NOT NULL,
+ `roleid` VARCHAR(64) NOT NULL,
+ `minutes` INT UNSIGNED NOT NULL,
+ PRIMARY KEY(`player`, `roleid`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- Role Time - Logging --
+-- Stores changes in role time --
+CREATE TABLE IF NOT EXISTS `rp_playtime_log` (
+ `player` INT(11),
+ `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
+ `roleid` VARCHAR(64) NOT NULL,
+ `delta` INT(11) NOT NULL,
+ `datetime` TIMESTAMP NOT NULL DEFAULT NOW() ON UPDATE NOW(),
+ PRIMARY KEY (`id`),
+ KEY `player` (`player`),
+ KEY `roleid` (`roleid`),
+ KEY `datetime` (`datetime`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+DELIMITER $$
+CREATE TRIGGER `playtimeTlogupdate` AFTER UPDATE ON `rp_playtime` FOR EACH ROW BEGIN INSERT into `rp_playtime_log` (player, roleid, delta) VALUES (NEW.player, NEW.roleid, NEW.minutes-OLD.minutes);
+END
+$$
+CREATE TRIGGER `playtimeTloginsert` AFTER INSERT ON `rp_playtime` FOR EACH ROW BEGIN INSERT into `rp_playtime_log` (player, roleid, delta) VALUES (NEW.player, NEW.roleid, NEW.minutes);
+END
+$$
+CREATE TRIGGER `playtimeTlogdelete` AFTER DELETE ON `rp_playtime` FOR EACH ROW BEGIN INSERT into `rp_playtime_log` (player, roleid, delta) VALUES (OLD.player, OLD.roleid, 0-OLD.minutes);
+END
+$$
+DELIMITER ;
+
+-- Security - Ipintel --
+
+-- Ipintel Cache Table --
+-- Stores cache entries for IPIntel --
+-- IP is in INET_ATON. --
+CREATE TABLE IF NOT EXISTS `rp_ipintel` (
+ `ip` INT(10) unsigned NOT NULL,
+ `date` TIMESTAMP NOT NULL DEFAULT NOW() ON UPDATE NOW(),
+ `intel` double NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ip`),
+ KEY `idx_ipintel` (`ip`, `intel`, `date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
--
-- Table structure for table `round`
diff --git a/citadel.dme b/citadel.dme
index 6bba67870eb..1a87bd3830d 100644
--- a/citadel.dme
+++ b/citadel.dme
@@ -122,6 +122,7 @@
#include "code\__DEFINES\atmospherics\pipes.dm"
#include "code\__DEFINES\cargo\supply.dm"
#include "code\__DEFINES\client\player_flags.dm"
+#include "code\__DEFINES\client\playtime.dm"
#include "code\__DEFINES\color\color.dm"
#include "code\__DEFINES\color\colour_priority.dm"
#include "code\__DEFINES\color\lights.dm"
@@ -476,18 +477,19 @@
#include "code\controllers\configuration\config_entry.dm"
#include "code\controllers\configuration\configuration.dm"
#include "code\controllers\configuration\whitelists.dm"
-#include "code\controllers\configuration\entries\bot.dm"
-#include "code\controllers\configuration\entries\comms.dm"
-#include "code\controllers\configuration\entries\compile.dm"
-#include "code\controllers\configuration\entries\dbconfig.dm"
-#include "code\controllers\configuration\entries\fail2topic.dm"
-#include "code\controllers\configuration\entries\game_options.dm"
+#include "code\controllers\configuration\entries\admin.dm"
+#include "code\controllers\configuration\entries\chat_bridge.dm"
+#include "code\controllers\configuration\entries\cross_server.dm"
+#include "code\controllers\configuration\entries\database.dm"
+#include "code\controllers\configuration\entries\game.dm"
#include "code\controllers\configuration\entries\general.dm"
#include "code\controllers\configuration\entries\health.dm"
#include "code\controllers\configuration\entries\lobby.dm"
#include "code\controllers\configuration\entries\logging.dm"
#include "code\controllers\configuration\entries\photography.dm"
+#include "code\controllers\configuration\entries\playtime.dm"
#include "code\controllers\configuration\entries\resources.dm"
+#include "code\controllers\configuration\entries\security.dm"
#include "code\controllers\configuration\entries\shadowban.dm"
#include "code\controllers\configuration\entries\skills.dm"
#include "code\controllers\configuration\entries\urls.dm"
@@ -520,6 +522,7 @@
#include "code\controllers\subsystem\icon_smooth.dm"
#include "code\controllers\subsystem\inactivity.dm"
#include "code\controllers\subsystem\input.dm"
+#include "code\controllers\subsystem\ipintel.dm"
#include "code\controllers\subsystem\legacy_atc.dm"
#include "code\controllers\subsystem\legacy_lore.dm"
#include "code\controllers\subsystem\lighting.dm"
@@ -538,6 +541,7 @@
#include "code\controllers\subsystem\ping.dm"
#include "code\controllers\subsystem\planets.dm"
#include "code\controllers\subsystem\plants.dm"
+#include "code\controllers\subsystem\playtime.dm"
#include "code\controllers\subsystem\radiation.dm"
#include "code\controllers\subsystem\repository.dm"
#include "code\controllers\subsystem\server_maint.dm"
@@ -2208,22 +2212,30 @@
#include "code\modules\catalogue\cataloguer.dm"
#include "code\modules\catalogue\cataloguer_visuals.dm"
#include "code\modules\catalogue\cataloguer_vr.dm"
-#include "code\modules\client\client procs_vr.dm"
#include "code\modules\client\client.dm"
#include "code\modules\client\client_data.dm"
#include "code\modules\client\client_procs.dm"
+#include "code\modules\client\connection.dm"
#include "code\modules\client\cutscene.dm"
+#include "code\modules\client\legacy.dm"
+#include "code\modules\client\perspective.dm"
#include "code\modules\client\player_data.dm"
+#include "code\modules\client\security.dm"
#include "code\modules\client\spam_prevention.dm"
#include "code\modules\client\statpanel.dm"
#include "code\modules\client\throttling.dm"
#include "code\modules\client\ui_style.dm"
#include "code\modules\client\viewport.dm"
#include "code\modules\client\wrappers.dm"
+#include "code\modules\client\onboarding\_onboarding.dm"
+#include "code\modules\client\onboarding\age_verification.dm"
+#include "code\modules\client\onboarding\panic_bunker.dm"
+#include "code\modules\client\onboarding\security_checks.dm"
#include "code\modules\client\verbs\minimap.dm"
#include "code\modules\client\verbs\ooc.dm"
#include "code\modules\client\verbs\panic_bunker_player.dm"
#include "code\modules\client\verbs\ping.dm"
+#include "code\modules\client\verbs\preferences.dm"
#include "code\modules\client\verbs\view.dm"
#include "code\modules\clothing\chameleon.dm"
#include "code\modules\clothing\clothing.dm"
diff --git a/code/__DEFINES/admin/bans.dm b/code/__DEFINES/admin/bans.dm
index aa556a5c9c7..77acce3eade 100644
--- a/code/__DEFINES/admin/bans.dm
+++ b/code/__DEFINES/admin/bans.dm
@@ -1,6 +1,5 @@
//? roleban types
/// full server ban - currently just a shim to go to legacy isbanned, eventually just will go to server_ban.dm
-
#define BAN_ROLE_SERVER "server"
/// OOC + LOOC + deadchat ban
#define BAN_ROLE_OOC "ooc"
diff --git a/code/__DEFINES/client/player_flags.dm b/code/__DEFINES/client/player_flags.dm
index e3b045658a0..f6e4c37d50f 100644
--- a/code/__DEFINES/client/player_flags.dm
+++ b/code/__DEFINES/client/player_flags.dm
@@ -1,7 +1,13 @@
//! player flags
-/// exempt from any job timelock system
+/// exempt from any job timelock system: this includes the VPN bunker!
#define PLAYER_FLAG_JEXP_EXEMPT (1<<0)
+/// age verified
+#define PLAYER_FLAG_AGE_VERIFIED (1<<1)
+/// connected, recorded, and *not* blocked through panic bunker when operating in connection mode
+#define PLAYER_FLAG_CONSIDERED_SEEN (1<<2)
DEFINE_BITFIELD(player_flags, list(
BITFIELD(PLAYER_FLAG_JEXP_EXEMPT),
+ BITFIELD(PLAYER_FLAG_AGE_VERIFIED),
+ BITFIELD(PLAYER_FLAG_CONSIDERED_SEEN),
))
diff --git a/code/__DEFINES/client/playtime.dm b/code/__DEFINES/client/playtime.dm
new file mode 100644
index 00000000000..bd3e669fea3
--- /dev/null
+++ b/code/__DEFINES/client/playtime.dm
@@ -0,0 +1,11 @@
+/// playtime key for alive
+#define PLAYER_PLAYTIME_LIVING "living"
+/// playtime key for died/as observer from a dead character
+#define PLAYER_PLAYTIME_DEAD "dead"
+/// playtime key for observer (but not because they died ofcourse)
+#define PLAYER_PLAYTIME_OBSERVER "observer"
+/// playtime key for lobby
+#define PLAYER_PLAYTIME_LOBBY "lobby"
+/// playtime key for role id
+#define PLAYER_PLAYTIME_ROLE(id) "role-[id]"
+
diff --git a/code/__DEFINES/controllers/_subsystems.dm b/code/__DEFINES/controllers/_subsystems.dm
index 4e09edfc09c..4d5067ecd71 100644
--- a/code/__DEFINES/controllers/_subsystems.dm
+++ b/code/__DEFINES/controllers/_subsystems.dm
@@ -78,6 +78,7 @@ DEFINE_BITFIELD(runlevels, list(
// todo: tg init brackets
#define INIT_ORDER_FAIL2TOPIC 200
+#define INIT_ORDER_IPINTEL 197
#define INIT_ORDER_TIMER 195
#define INIT_ORDER_DBCORE 190
#define INIT_ORDER_EARLY_INIT 185
diff --git a/code/__DEFINES/preferences/savefiles.dm b/code/__DEFINES/preferences/savefiles.dm
index edd5bce2641..7af4232f0ea 100644
--- a/code/__DEFINES/preferences/savefiles.dm
+++ b/code/__DEFINES/preferences/savefiles.dm
@@ -3,7 +3,7 @@
//* We store this on savefile because you can handle global migrations
//* and advanced direct savefile migrations directly with this.
#define SAVEFILE_VERSION_MIN 8
-#define SAVEFILE_VERSION_MAX 15
+#define SAVEFILE_VERSION_MAX 16
//! Character version - stored in character data list
//* Slot gets wiped if version < MIN
diff --git a/code/__HELPERS/_logging.dm b/code/__HELPERS/_logging.dm
index a4daaa2d91c..14127fab783 100644
--- a/code/__HELPERS/_logging.dm
+++ b/code/__HELPERS/_logging.dm
@@ -188,6 +188,9 @@ GLOBAL_LIST_INIT(testing_global_profiler, list("_PROFILE_NAME" = "Global"))
GLOB.round_text_log += "([time_stamp()]) ([user]) LOOC: - [text]"
+/proc/log_ipintel(text)
+ WRITE_LOG(GLOB.world_runtime_log, "IPINTEL: [text]")
+
/proc/log_vote(text)
if (config_legacy.log_vote)
WRITE_LOG(GLOB.world_game_log, "VOTE: [text]")
@@ -213,6 +216,9 @@ GLOBAL_LIST_INIT(testing_global_profiler, list("_PROFILE_NAME" = "Global"))
/proc/log_reagent_transfer(text)
log_reagent("TRANSFER: [text]")
+/proc/log_security(text)
+ WRITE_LOG(GLOB.world_game_log, "SECURITY: [text]")
+
/proc/log_subsystem(subsystem, text)
WRITE_LOG(GLOB.subsystem_log, "[subsystem]: [text]")
diff --git a/code/__HELPERS/time.dm b/code/__HELPERS/time.dm
index c12e3f617c3..6522ab63b7c 100644
--- a/code/__HELPERS/time.dm
+++ b/code/__HELPERS/time.dm
@@ -1,3 +1,6 @@
+GLOBAL_VAR_INIT(startup_year, text2num(time2text(world.time, "YYYY")))
+GLOBAL_VAR_INIT(startup_month, text2num(time2text(world.time, "MM")))
+GLOBAL_VAR_INIT(startup_day, text2num(time2text(world.time, "DD")))
#define TimeOfGame (get_game_time())
#define TimeOfTick (TICK_USAGE*0.01*world.tick_lag)
diff --git a/code/controllers/configuration/entries/admin.dm b/code/controllers/configuration/entries/admin.dm
new file mode 100644
index 00000000000..b95849d3bb3
--- /dev/null
+++ b/code/controllers/configuration/entries/admin.dm
@@ -0,0 +1 @@
+/datum/config_entry/flag/enable_localhost_rank
diff --git a/code/controllers/configuration/entries/bot.dm b/code/controllers/configuration/entries/chat_bridge.dm
similarity index 100%
rename from code/controllers/configuration/entries/bot.dm
rename to code/controllers/configuration/entries/chat_bridge.dm
diff --git a/code/controllers/configuration/entries/compile.dm b/code/controllers/configuration/entries/compile.dm
deleted file mode 100644
index 78f541e8002..00000000000
--- a/code/controllers/configuration/entries/compile.dm
+++ /dev/null
@@ -1,2 +0,0 @@
-/// Enable or disable the toast notification when the the instance finishes initializing.
-/datum/config_entry/flag/toast_notification_on_init
diff --git a/code/controllers/configuration/entries/comms.dm b/code/controllers/configuration/entries/cross_server.dm
similarity index 91%
rename from code/controllers/configuration/entries/comms.dm
rename to code/controllers/configuration/entries/cross_server.dm
index 1fae28a08ab..d8d29b91312 100644
--- a/code/controllers/configuration/entries/comms.dm
+++ b/code/controllers/configuration/entries/cross_server.dm
@@ -4,6 +4,7 @@
/datum/config_entry/string/comms_key/ValidateAndSet(str_val)
return str_val != "default_pwd" && length(str_val) > 6 && ..()
+// todo: remove
/datum/config_entry/keyed_list/cross_server_bunker_override
key_mode = KEY_MODE_TEXT
value_mode = VALUE_MODE_TEXT
@@ -23,4 +24,5 @@
/datum/config_entry/flag/allow_cross_server_bunker_override
protection = CONFIG_ENTRY_LOCKED
+// todo: remove, cluster staging/organization should be in a database
/datum/config_entry/string/cross_comms_name
diff --git a/code/controllers/configuration/entries/dbconfig.dm b/code/controllers/configuration/entries/database.dm
similarity index 100%
rename from code/controllers/configuration/entries/dbconfig.dm
rename to code/controllers/configuration/entries/database.dm
diff --git a/code/controllers/configuration/entries/fail2topic.dm b/code/controllers/configuration/entries/fail2topic.dm
deleted file mode 100644
index 5a7ebbbd07f..00000000000
--- a/code/controllers/configuration/entries/fail2topic.dm
+++ /dev/null
@@ -1,19 +0,0 @@
-/datum/config_entry/number/fail2topic_rate_limit
- default = 10 //Deciseconds
-
-/datum/config_entry/number/fail2topic_max_fails
- default = 5
-
-/datum/config_entry/string/fail2topic_rule_name
- default = "_dd_fail2topic"
- protection = CONFIG_ENTRY_LOCKED //Affects physical server configuration, no touchies!!
-
-/datum/config_entry/flag/fail2topic_enabled
- default = TRUE
-
-/datum/config_entry/number/topic_max_size
- default = 1048576
-
-/datum/config_entry/keyed_list/topic_rate_limit_whitelist
- key_mode = KEY_MODE_TEXT
- value_mode = VALUE_MODE_FLAG
diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game.dm
similarity index 100%
rename from code/controllers/configuration/entries/game_options.dm
rename to code/controllers/configuration/entries/game.dm
diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm
index c8e834abc2d..d9882912bae 100644
--- a/code/controllers/configuration/entries/general.dm
+++ b/code/controllers/configuration/entries/general.dm
@@ -1,10 +1,6 @@
/datum/config_entry/flag/minimaps_enabled
default = TRUE
-/datum/config_entry/number/max_bunker_days
- default = 7
- min_val = 1
-
/datum/config_entry/string/invoke_youtubedl
protection = CONFIG_ENTRY_LOCKED | CONFIG_ENTRY_HIDDEN
@@ -32,20 +28,9 @@
default = null
min_val = 0
-/datum/config_entry/string/community_shortname
-
-/datum/config_entry/string/community_link
-
-/datum/config_entry/string/tagline
- default = "
Roleplay focused 18+ server with extensive species choices."
-
-/datum/config_entry/flag/usetaglinestrings
-
/datum/config_entry/flag/cache_assets
default = TRUE
-/datum/config_entry/flag/show_irc_name
-
/// allows admins with relevant permissions to have their own ooc colour
/datum/config_entry/flag/allow_admin_ooccolor
default = TRUE
@@ -53,3 +38,6 @@
/datum/config_entry/number/rounds_until_hard_restart
default = -1
min_val = 0
+
+/// Enable or disable the toast notification when the the instance finishes initializing.
+/datum/config_entry/flag/toast_notification_on_init
diff --git a/code/controllers/configuration/entries/lobby.dm b/code/controllers/configuration/entries/lobby.dm
index 3531599f3ed..575a3b888c2 100644
--- a/code/controllers/configuration/entries/lobby.dm
+++ b/code/controllers/configuration/entries/lobby.dm
@@ -23,3 +23,12 @@
/// Enforce flavortext
/datum/config_entry/flag/enforce_flavor_text
+
+/datum/config_entry/string/community_shortname
+
+/datum/config_entry/string/community_link
+
+/datum/config_entry/string/tagline
+ default = "
Roleplay focused 18+ server with extensive species choices."
+
+/datum/config_entry/flag/usetaglinestrings
diff --git a/code/controllers/configuration/entries/logging.dm b/code/controllers/configuration/entries/logging.dm
index f564e91e224..8d9e36636d8 100644
--- a/code/controllers/configuration/entries/logging.dm
+++ b/code/controllers/configuration/entries/logging.dm
@@ -1,94 +1,5 @@
/datum/config_entry/flag/emergency_tgui_logging
default = FALSE
-/// log messages sent in OOC
-/datum/config_entry/flag/log_ooc
-
-/// log login/logout
-/datum/config_entry/flag/log_access
-
/// Config entry which special logging of failed logins under suspicious circumstances.
/datum/config_entry/flag/log_suspicious_login
-
-/// log client say
-/datum/config_entry/flag/log_say
-
-/// log admin actions
-/datum/config_entry/flag/log_admin
- protection = CONFIG_ENTRY_LOCKED
-
-/// log prayers
-/datum/config_entry/flag/log_prayer
-
-/// log silicons
-/datum/config_entry/flag/log_silicon
-
-/datum/config_entry/flag/log_law
- deprecated_by = /datum/config_entry/flag/log_silicon
-
-/datum/config_entry/flag/log_law/DeprecationUpdate(value)
- return value
-
-/// log usage of tools
-/datum/config_entry/flag/log_tools
-
-/// log game events
-/datum/config_entry/flag/log_game
-
-/// log mech data
-/datum/config_entry/flag/log_mecha
-
-/// log virology data
-/datum/config_entry/flag/log_virus
-
-/// log assets
-/datum/config_entry/flag/log_asset
-
-/// log voting
-/datum/config_entry/flag/log_vote
-
-/// log client whisper
-/datum/config_entry/flag/log_whisper
-
-/// log attack messages
-/datum/config_entry/flag/log_attack
-
-/// log emotes
-/datum/config_entry/flag/log_emote
-
-/// log economy actions
-/datum/config_entry/flag/log_econ
-
-/// log traitor objectives
-/datum/config_entry/flag/log_traitor
-
-/// log admin chat messages
-/datum/config_entry/flag/log_adminchat
- protection = CONFIG_ENTRY_LOCKED
-
-/// log pda messages
-/datum/config_entry/flag/log_pda
-
-/// log uplink/spellbook/codex ciatrix purchases and refunds
-/datum/config_entry/flag/log_uplink
-
-/// log telecomms messages
-/datum/config_entry/flag/log_telecomms
-
-/// log certain expliotable parrots and other such fun things in a JSON file of twitter valid phrases.
-/datum/config_entry/flag/log_twitter
-
-/// log all world.Topic() calls
-/datum/config_entry/flag/log_world_topic
-
-/// log crew manifest to separate file
-/datum/config_entry/flag/log_manifest
-
-/// log roundstart divide occupations debug information to a file
-/datum/config_entry/flag/log_job_debug
-
-/// log shuttle related actions, ie shuttle computers, shuttle manipulator, emergency console
-/datum/config_entry/flag/log_shuttle
-
-/// logs all timers in buckets on automatic bucket reset (Useful for timer debugging)
-/datum/config_entry/flag/log_timers_on_bucket_reset
diff --git a/code/controllers/configuration/entries/playtime.dm b/code/controllers/configuration/entries/playtime.dm
new file mode 100644
index 00000000000..409aa5482c6
--- /dev/null
+++ b/code/controllers/configuration/entries/playtime.dm
@@ -0,0 +1,8 @@
+/// Playtime tracking enabled
+/datum/config_entry/flag/playtime_tracking
+
+/// Playtime restrictions enabled
+/datum/config_entry/flag/job_check_playtime
+
+/// Account age restrictions enabled
+/datum/config_entry/flag/job_check_account_age
diff --git a/code/controllers/configuration/entries/security.dm b/code/controllers/configuration/entries/security.dm
new file mode 100644
index 00000000000..ad14d7b9c9c
--- /dev/null
+++ b/code/controllers/configuration/entries/security.dm
@@ -0,0 +1,79 @@
+//* Fail2Topic - Topic DoS Guard System *//
+
+/datum/config_entry/number/fail2topic_rate_limit
+ default = 10 //Deciseconds
+
+/datum/config_entry/number/fail2topic_max_fails
+ default = 5
+
+/datum/config_entry/string/fail2topic_rule_name
+ default = "_dd_fail2topic"
+ protection = CONFIG_ENTRY_LOCKED //Affects physical server configuration, no touchies!!
+
+/datum/config_entry/flag/fail2topic_enabled
+ default = TRUE
+
+/datum/config_entry/number/topic_max_size
+ default = 1048576
+
+/datum/config_entry/keyed_list/topic_rate_limit_whitelist
+ key_mode = KEY_MODE_TEXT
+ value_mode = VALUE_MODE_FLAG
+
+//* IPIntel - VPN Intelligence System *//
+
+/datum/config_entry/flag/ipintel_enabled
+
+/datum/config_entry/string/ipintel_email
+
+/datum/config_entry/string/ipintel_email/ValidateAndSet(str_val)
+ return str_val != "ch@nge.me" && ..()
+
+/datum/config_entry/number/ipintel_rating_bad
+ config_entry_value = 1
+ integer = FALSE
+ min_val = 0
+ max_val = 1
+
+/datum/config_entry/number/ipintel_save_good
+ config_entry_value = 12
+ min_val = 0
+
+/datum/config_entry/number/ipintel_save_bad
+ config_entry_value = 1
+ min_val = 0
+
+/datum/config_entry/string/ipintel_domain
+ config_entry_value = "check.getipintel.net"
+
+//* Age Verification System - requires DB *//
+
+/datum/config_entry/flag/age_verification //are we using the automated age verification which asks users if they're 18+?
+
+/datum/config_entry/flag/age_verification_autoban
+
+//* Miscellaneous Security Checks *//
+
+// todo: implement
+/// Check for CID Randomizers
+/datum/config_entry/flag/check_cid_randomizer
+
+//* Panic Bunker - requires DB *//
+
+/// Full panic bunker - people who have never connected/played before get bounced off
+/datum/config_entry/flag/panic_bunker
+
+/// Partial panic bunker - Only apply panic_bunker to detected VPNs from IPIntel
+/datum/config_entry/flag/vpn_bunker
+
+/// Instead of "connected successfully at all", we check for playtime if this is set. 0 to disable.
+/datum/config_entry/number/panic_bunker_playtime
+
+/// Message shown to players who try to connect during panic bunker.
+/datum/config_entry/string/panic_bunker_message
+ default = "Sorry but the server is currently not accepting connections from never before seen players."
+
+/// Days to keep someone in bunker bypass once whitelisted
+/datum/config_entry/number/panic_bunker_bypass
+ default = 7
+ min_val = 1
diff --git a/code/controllers/configuration_old/configuration.dm b/code/controllers/configuration_old/configuration.dm
index 4d3f30239f8..19b43e4cca2 100644
--- a/code/controllers/configuration_old/configuration.dm
+++ b/code/controllers/configuration_old/configuration.dm
@@ -107,18 +107,8 @@
var/allow_extra_antags = 0
var/guests_allowed = 1
var/debugparanoid = 0
- var/panic_bunker = 0
- var/panic_bunker_message = "Sorry, this server is not accepting connections from never seen before players."
var/paranoia_logging = 0
- var/ip_reputation = FALSE //Should we query IPs to get scores? Generates HTTP traffic to an API service.
- var/ipr_email //Left null because you MUST specify one otherwise you're making the internet worse.
- var/ipr_block_bad_ips = FALSE //Should we block anyone who meets the minimum score below? Otherwise we just log it (If paranoia logging is on, visibly in chat).
- var/ipr_bad_score = 1 //The API returns a value between 0 and 1 (inclusive), with 1 being 'definitely VPN/Tor/Proxy'. Values equal/above this var are considered bad.
- var/ipr_allow_existing = FALSE //Should we allow known players to use VPNs/Proxies? If the player is already banned then obviously they still can't connect.
- var/ipr_minimum_age = 5
- var/ipqualityscore_apikey //API key for ipqualityscore.com
-
var/serverurl
var/server
var/banappeals
@@ -175,8 +165,6 @@
var/admin_legacy_system = 0 //Defines whether the server uses the legacy admin system with admins.txt or the SQL system. Config option in
var/ban_legacy_system = 0 //Defines whether the server uses the legacy banning system with the files in /data or the SQL system. Config option in config_legacy.txt
- var/use_age_restriction_for_jobs = 0 //Do jobs use account age restrictions? --requires database
- var/use_age_restriction_for_antags = 0 //Do antags use account age restrictions? --requires database
var/simultaneous_pm_warning_timeout = 100
@@ -311,12 +299,6 @@
if ("hub_visibility") //CITADEL CHANGE - ADDS HUB CONFIG
config_legacy.hub_visibility = 1
- if ("use_age_restriction_for_jobs")
- config_legacy.use_age_restriction_for_jobs = 1
-
- if ("use_age_restriction_for_antags")
- config_legacy.use_age_restriction_for_antags = 1
-
if ("jobs_have_minimal_access")
config_legacy.jobs_have_minimal_access = 1
@@ -771,33 +753,9 @@
if("radiation_lower_limit")
radiation_lower_limit = text2num(value)
- if ("panic_bunker")
- config_legacy.panic_bunker = 1
-
- if ("panic_bunker_message")
- config_legacy.panic_bunker_message = value
-
if ("paranoia_logging")
config_legacy.paranoia_logging = 1
- if("ip_reputation")
- config_legacy.ip_reputation = 1
-
- if("ipr_email")
- config_legacy.ipr_email = value
-
- if("ipr_block_bad_ips")
- config_legacy.ipr_block_bad_ips = 1
-
- if("ipr_bad_score")
- config_legacy.ipr_bad_score = text2num(value)
-
- if("ipr_allow_existing")
- config_legacy.ipr_allow_existing = 1
-
- if("ipr_minimum_age")
- config_legacy.ipr_minimum_age = text2num(value)
-
if("minute_click_limit")
config_legacy.minute_click_limit = text2num(value)
diff --git a/code/controllers/subsystem/input.dm b/code/controllers/subsystem/input.dm
index ceb8f26dea4..f05c86e92d6 100644
--- a/code/controllers/subsystem/input.dm
+++ b/code/controllers/subsystem/input.dm
@@ -112,8 +112,9 @@ SUBSYSTEM_DEF(input)
/datum/controller/subsystem/input/fire()
var/list/clients = GLOB.clients // Let's sing the list cache song
- for(var/i in 1 to clients.len)
- var/client/C = clients[i]
+ for(var/client/C as anything in clients)
+ if(!C.initialized)
+ continue
C.keyLoop()
/// *sigh
diff --git a/code/controllers/subsystem/ipintel.dm b/code/controllers/subsystem/ipintel.dm
new file mode 100644
index 00000000000..e35e0c9e394
--- /dev/null
+++ b/code/controllers/subsystem/ipintel.dm
@@ -0,0 +1,217 @@
+/**
+ * IPIntel Subsystem
+ */
+SUBSYSTEM_DEF(ipintel)
+ name = "IPIntel"
+ init_order = INIT_ORDER_IPINTEL
+ subsystem_flags = SS_NO_FIRE
+
+ /// is ipintel enabled?
+ var/enabled = FALSE
+ /// threshold for blocking vpns
+ var/vpn_threshold
+ /// ip (as client.address form) to cache entry
+ var/static/list/vpn_cache = list()
+ /// current consequetive errors
+ var/consequetive_errors = 0
+ /// next time before we try again once errored
+ var/next_attempt = 0
+ /// retry delay
+ var/retry_delay = 4 SECONDS
+ /// max retries
+ var/max_retries = 1
+
+/datum/controller/subsystem/ipintel/OnConfigLoad()
+ . = ..()
+ enabled = !!CONFIG_GET(flag/ipintel_enabled)
+ consequetive_errors = 0
+ next_attempt = 0
+ vpn_threshold = CONFIG_GET(number/ipintel_rating_bad)
+
+/datum/controller/subsystem/ipintel/proc/vpn_connection_check(address, ckey)
+ if(!CONFIG_GET(flag/ipintel_enabled))
+ return
+ var/score = vpn_score(address)
+ if(isnull(score))
+ log_and_message_admins("Unable to check IPIntel for [ckey].")
+ log_access("[ckey] ([address]) could not be checked by IPIntel.")
+ return
+ if(score >= vpn_threshold)
+ log_and_message_admins("[ckey] detected to likely be using a vpn ([score] >= [vpn_threshold])")
+ log_access("[ckey] ([address]) is likely using a vpn ([score] >= [vpn_threshold])")
+
+/datum/controller/subsystem/ipintel/proc/vpn_score(address)
+ var/datum/ipintel/cached = vpn_cache[address]
+ if(isnull(cached))
+ var/datum/ipintel/fetched = ipintel_cache_fetch(address)
+ if(!isnull(fetched))
+ log_ipintel("successfully fetched cache for [address]")
+ cached = fetched
+ vpn_cache[address] = fetched
+ if(cached?.is_valid())
+ log_ipintel("using valid cache for [address]")
+ return cached.intel
+ log_ipintel("using api for [address]")
+ var/score = ipintel_query(address)
+ if(isnull(score))
+ return
+ var/datum/ipintel/result = new
+ result.intel = score
+ result.address = address
+ ipintel_cache_store(result)
+ vpn_cache[address] = result
+ return result.intel
+
+/datum/controller/subsystem/ipintel/proc/vpn_check(address)
+ return vpn_score(address) >= vpn_threshold
+
+/datum/controller/subsystem/ipintel/proc/ipintel_query(address, retries)
+ PRIVATE_PROC(TRUE)
+ // bruh it's localhost
+ if(address == "127.0.0.1" || isnull(address))
+ return 0
+ // no flooding API without cache being available
+ if(!SSdbcore.Connect())
+ log_ipintel("ipintel: no DB")
+ message_admins("IPIntel failed due to lack of database. Yell at your hosts.")
+ return
+ if(retries > max_retries)
+ log_ipintel("ipintel: bailing for [address] due to [retries] > [max_retries].")
+ return
+ if(!address)
+ return
+ if(next_attempt > REALTIMEOFDAY)
+ return
+ if(!enabled)
+ return
+
+ var/list/http[] = world.Export("http://[CONFIG_GET(string/ipintel_domain)]/check.php?ip=[address]&contact=[CONFIG_GET(string/ipintel_email)]&format=json&flags=f")
+
+ if(isnull(http))
+ ipintel_error(address, "Unable to connect", retries)
+ retries++
+ sleep(retry_delay)
+ return .()
+
+ var/status = text2num(http["STATUS"])
+
+ if(status == 200)
+ // success
+ var/response = json_decode(file2text(http["CONTENT"]))
+ if(isnull(response))
+ ipintel_error(address, "Code 400, but no response. Bailing out.")
+ return
+ if(response["status"] == "success")
+ var/parsed = text2num(response["result"])
+ if(isnum(parsed))
+ // reset error counts
+ consequetive_errors = 0
+ next_attempt = 0
+ return parsed
+ ipintel_error(address, "Bad intel from server: [response["result"]]", retries)
+ retries++
+ sleep(retry_delay)
+ return .()
+ else
+ ipintel_error(address, "Bad response from server: [response["status"]]", retries)
+ retries++
+ sleep(retry_delay)
+ return .()
+ else if(status == 429)
+ // ratelimited
+ ipintel_error(address, "Code 429: Ratelimited")
+ return
+ else
+ ipintel_error(address, "Code [status]: Unknown", retries)
+ retries++
+ sleep(retry_delay)
+ return .()
+
+/datum/controller/subsystem/ipintel/proc/ipintel_cache_fetch(address)
+ PRIVATE_PROC(TRUE)
+ if(!SSdbcore.Connect())
+ return
+ // admin proccall guard override - there's no volatile args here
+ var/old_usr = usr
+ usr = null
+ . = ipintel_cache_fetch_impl(address)
+ usr = old_usr
+
+/datum/controller/subsystem/ipintel/proc/ipintel_cache_fetch_impl(address)
+ PRIVATE_PROC(TRUE)
+ var/datum/db_query/fetch = SSdbcore.NewQuery(
+ "SELECT date, intel, TIMESTAMPDIFF(MINUTE,date,NOW()) FROM [format_table_name("ipintel")] WHERE ip = INET_ATON(:ip)",
+ list(
+ "ip" = address,
+ )
+ )
+ fetch.Execute()
+ if(fetch.NextRow())
+ var/datum/ipintel/fetched = new /datum/ipintel
+ . = fetched
+ fetched.address = address
+ fetched.intel = text2num(fetch.item[2])
+ fetched.cached_timestamp = fetch.item[1]
+ fetched.cached_realtime = world.realtime - (text2num(fetch.item[3]) * 10 * 60)
+ qdel(fetch)
+
+/datum/controller/subsystem/ipintel/proc/ipintel_cache_store(datum/ipintel/entry)
+ PRIVATE_PROC(TRUE)
+ if(!SSdbcore.Connect())
+ return
+ // admin proccall guard override - there's no volatile args here
+ var/old_usr = usr
+ usr = null
+ . = ipintel_cache_store_impl(entry)
+ usr = old_usr
+
+/datum/controller/subsystem/ipintel/proc/ipintel_cache_store_impl(datum/ipintel/entry)
+ PRIVATE_PROC(TRUE)
+ var/datum/db_query/update = SSdbcore.NewQuery(
+ "INSERT INTO [format_table_name("ipintel")] (ip, intel) VALUES (INET_ATON(:ip), :intel) \
+ ON DUPLICATE KEY UPDATE intel = VALUES(intel), date = NOW()",
+ list(
+ "ip" = entry.address,
+ "intel" = entry.intel,
+ )
+ )
+ update.Execute()
+ qdel(update)
+
+/datum/controller/subsystem/ipintel/proc/ipintel_error(address, error, retries)
+ PRIVATE_PROC(TRUE)
+ var/str = "IPIntel error handling on [address]: "
+ if(retries)
+ consequetive_errors++
+ var/how_long = consequetive_errors * 2 MINUTES
+ str += "Could not check [address]. Disabling IPIntel for [DisplayTimeText(how_long)]."
+ next_attempt = REALTIMEOFDAY + how_long
+ else
+ str += "Attempting to retry."
+
+/datum/ipintel
+ var/address
+ var/intel
+ var/cached_timestamp
+ var/cached_realtime
+
+/datum/ipintel/New()
+ cached_timestamp = time_stamp()
+ cached_realtime = world.realtime
+
+/datum/ipintel/proc/is_valid()
+ . = FALSE
+ var/allowable_hours = intel < SSipintel.vpn_threshold? CONFIG_GET(number/ipintel_save_good) : CONFIG_GET(number/ipintel_save_bad)
+ return world.realtime < cached_realtime + (allowable_hours HOURS)
+
+/**
+/proc/ipintel_handle_error(error, ip, retryed)
+ if (retryed)
+ SSipintel.errors++
+ error += " Could not check [ip]. Disabling IPINTEL for [SSipintel.errors] minute[( SSipintel.errors == 1 ? "" : "s" )]"
+ SSipintel.throttle = world.timeofday + (10 * 120 * SSipintel.errors)
+ else
+ error += " Attempting retry on [ip]."
+ log_ipintel(error)
+
+ */
diff --git a/code/controllers/subsystem/persistence/bunker.dm b/code/controllers/subsystem/persistence/bunker.dm
index 0ad95dc8782..79ef80fc726 100644
--- a/code/controllers/subsystem/persistence/bunker.dm
+++ b/code/controllers/subsystem/persistence/bunker.dm
@@ -20,5 +20,5 @@
var/list/json = json_decode(file2text(bunker_path))
GLOB.bunker_passthrough = json["data"]
for(var/ckey in GLOB.bunker_passthrough)
- if(daysSince(GLOB.bunker_passthrough[ckey]) >= CONFIG_GET(number/max_bunker_days))
+ if(daysSince(GLOB.bunker_passthrough[ckey]) >= CONFIG_GET(number/panic_bunker_bypass))
GLOB.bunker_passthrough -= ckey
diff --git a/code/controllers/subsystem/playtime.dm b/code/controllers/subsystem/playtime.dm
new file mode 100644
index 00000000000..a13bea15e9a
--- /dev/null
+++ b/code/controllers/subsystem/playtime.dm
@@ -0,0 +1,85 @@
+/**
+ * playtime tracking system
+ *
+ * yes, the code is messy and probably shouldn't be half-in-subsystem and half-elsewhere, buuut
+ * whatever.
+ *
+ * todo: this can probably be optimized to be better at yielding instead of using dumb CHECK_TICKS.
+ */
+SUBSYSTEM_DEF(playtime)
+ name = "Playtime"
+ wait = 10 MINUTES
+ subsystem_flags = SS_NO_TICK_CHECK
+
+/datum/controller/subsystem/playtime/Shutdown()
+ flush_playtimes()
+ return ..()
+
+/datum/controller/subsystem/playtime/fire(resumed)
+ for(var/client/C in GLOB.clients)
+ if(!C.initialized)
+ continue
+ queue_playtimes(C)
+ CHECK_TICK
+ flush_playtimes()
+
+/datum/controller/subsystem/playtime/proc/flush_playtimes()
+ if(!SSdbcore.Connect())
+ return
+ // admin proccall guard override - there's no volatile args here
+ var/old_usr = usr
+ usr = null
+ . = flush_playtimes_impl()
+ usr = old_usr
+
+/datum/controller/subsystem/playtime/proc/flush_playtimes_impl()
+ var/list/built = list()
+ for(var/client/C in GLOB.clients)
+ if(!C.initialized)
+ continue
+ var/playerid = C.player.player_id
+ for(var/roleid in C.persistent.playtime_queued)
+ var/minutes = C.persistent.playtime_queued[roleid]
+ built[++built.len] = list(
+ "roleid" = roleid,
+ "minutes" = minutes,
+ "player" = playerid
+ )
+ C.persistent.playtime_queued = list()
+ SSdbcore.MassInsert(format_table_name("playtime"), built, duplicate_key = "ON DUPLICATE KEY UPDATE minutes = minutes + VALUES(minutes)")
+
+/**
+ * returns a list of playtime roles
+ */
+/datum/controller/subsystem/playtime/proc/playtime_for(mob/M)
+ if(isobserver(M))
+ var/mob/observer/dead/ghost = M
+ return list(ghost.started_as_observer? PLAYER_PLAYTIME_OBSERVER : PLAYER_PLAYTIME_DEAD)
+ else if(isnewplayer(M))
+ return list(PLAYER_PLAYTIME_LOBBY)
+ if(IS_DEAD(M))
+ . = list(PLAYER_PLAYTIME_DEAD)
+ else
+ . = list(PLAYER_PLAYTIME_LIVING)
+ var/best_effort_attempt_at_resolving_legacy_name_based_roles = M.mind?.assigned_role
+ var/datum/role/job/J = SSjob.job_by_title(best_effort_attempt_at_resolving_legacy_name_based_roles)
+ if(J)
+ . += PLAYER_PLAYTIME_ROLE(J.id)
+
+/datum/controller/subsystem/playtime/proc/queue_playtimes(client/C)
+ if(isnull(C))
+ return
+ if(!C.initialized)
+ CRASH("how was this called on an uninitialized client?")
+ var/list/playtimes = playtime_for(C.mob)
+ var/now = REALTIMEOFDAY
+ // deciseconds to minutes
+ var/since_last = round((now - C.persistent.playtime_last) * (1 / 10) * (1 / 60))
+ C.persistent.playtime_last = now
+ if(since_last < 0)
+ CRASH("how was since_last [since_last] < 0?")
+ if(!length(playtimes))
+ return
+ LAZYINITLIST(C.persistent.playtime_queued)
+ for(var/role in playtimes)
+ C.persistent.playtime_queued[role] += since_last
diff --git a/code/controllers/subsystem/tgui.dm b/code/controllers/subsystem/tgui.dm
index 2c617614445..0f949e91dd3 100644
--- a/code/controllers/subsystem/tgui.dm
+++ b/code/controllers/subsystem/tgui.dm
@@ -175,6 +175,7 @@ SUBSYSTEM_DEF(tgui)
* return datum/tgui The found UI.
*/
/datum/controller/subsystem/tgui/proc/get_open_ui(mob/user, datum/src_object)
+ RETURN_TYPE(/datum/tgui)
var/key = "[REF(src_object)]"
// No UIs opened for this src_object
if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list))
diff --git a/code/datums/world_topic.dm b/code/datums/world_topic.dm
index 6501ce2f1e2..a8c794b1865 100644
--- a/code/datums/world_topic.dm
+++ b/code/datums/world_topic.dm
@@ -94,70 +94,6 @@
send2irc("Panic Bunker", "AUTO BUNKER: [ckeytobypass] given access (incoming comms from [sender]).")
return "Success"
-
-/*
-/datum/world_topic/ahelp_relay
- keyword = "Ahelp"
- require_comms_key = TRUE
-
-/datum/world_topic/ahelp_relay/Run(list/input)
- relay_msg_admins("HELP: [input["source"]] [input["message_sender"]]: [input["message"]]")
-
-/datum/world_topic/comms_console
- keyword = "Comms_Console"
- require_comms_key = TRUE
-
-/datum/world_topic/comms_console/Run(list/input)
- minor_announce(input["message"], "Incoming message from [input["message_sender"]]")
- for(var/obj/machinery/computer/communications/CM in GLOB.machines)
- CM.overrideCooldown()
-
-/datum/world_topic/news_report
- keyword = "News_Report"
- require_comms_key = TRUE
-
-/datum/world_topic/news_report/Run(list/input)
- minor_announce(input["message"], "Breaking Update From [input["message_sender"]]")
-
-/datum/world_topic/server_hop
- keyword = "server_hop"
-
-/datum/world_topic/server_hop/Run(list/input)
- var/expected_key = input[keyword]
- for(var/mob/observer/dead/O in GLOB.GLOB.player_list)
- if(O.key == expected_key)
- if(O.client)
- new /atom/movable/screen/splash(O.client, TRUE)
- break
-
-/datum/world_topic/adminmsg
- keyword = "adminmsg"
- require_comms_key = TRUE
-
-/datum/world_topic/adminmsg/Run(list/input)
- return IrcPm(input[keyword], input["msg"], input["sender"])
-
-/datum/world_topic/namecheck
- keyword = "namecheck"
- require_comms_key = TRUE
-
-/datum/world_topic/namecheck/Run(list/input)
- //Oh this is a hack, someone refactor the functionality out of the chat command PLS
- var/datum/tgs_chat_command/namecheck/NC = new
- var/datum/tgs_chat_user/user = new
- user.friendly_name = input["sender"]
- user.mention = user.friendly_name
- return NC.Run(user, input["namecheck"])
-
-/datum/world_topic/adminwho
- keyword = "adminwho"
- require_comms_key = TRUE
-
-/datum/world_topic/adminwho/Run(list/input)
- return ircadminwho()
-
-*/
-
/datum/world_topic/jsonstatus
keyword = "jsonstatus"
diff --git a/code/game/antagonist/antagonist.dm b/code/game/antagonist/antagonist.dm
index 0d7c82ed724..02a595ad042 100644
--- a/code/game/antagonist/antagonist.dm
+++ b/code/game/antagonist/antagonist.dm
@@ -111,9 +111,9 @@
if(ghosts_only && !istype(player.current, /mob/observer/dead))
candidates -= player
log_debug(SPAN_DEBUG("[key_name(player)] is not eligible to become a [role_text]: Only ghosts may join as this role! They have been removed from the draft."))
- else if(config_legacy.use_age_restriction_for_antags && player.current.client.player_age < minimum_player_age)
- candidates -= player
- log_debug(SPAN_DEBUG("[key_name(player)] is not eligible to become a [role_text]: Is only [player.current.client.player_age] day\s old, has to be [minimum_player_age] day\s!"))
+ // else if(config_legacy.use_age_restriction_for_antags && player.current.client.player_age < minimum_player_age)
+ // candidates -= player
+ // log_debug(SPAN_DEBUG("[key_name(player)] is not eligible to become a [role_text]: Is only [player.current.client.player_age] day\s old, has to be [minimum_player_age] day\s!"))
else if(player.special_role)
candidates -= player
log_debug(SPAN_DEBUG("[key_name(player)] is not eligible to become a [role_text]: They already have a special role ([player.special_role])! They have been removed from the draft."))
diff --git a/code/game/verbs/advanced_who.dm b/code/game/verbs/advanced_who.dm
index fa1040db7bc..d19d6ac5d86 100644
--- a/code/game/verbs/advanced_who.dm
+++ b/code/game/verbs/advanced_who.dm
@@ -27,8 +27,8 @@
entry += " - DEAD"
var/age
- if(isnum(C.player_age))
- age = C.player_age
+ if(isnum(C.player.player_age))
+ age = C.player.player_age
else
age = 0
diff --git a/code/game/verbs/who.dm b/code/game/verbs/who.dm
index 8c325f0c303..ae584e95569 100644
--- a/code/game/verbs/who.dm
+++ b/code/game/verbs/who.dm
@@ -11,6 +11,12 @@
var/entry = "\t[C.key]"
if(C.holder && C.holder.fakekey)
entry += " (as [C.holder.fakekey])"
+ if(!C.initialized)
+ entry += " - [SPAN_BOLDANNOUNCE("UNINITIALIZED!")]"
+ continue
+ if(isnull(C.mob))
+ entry += " - [SPAN_BOLDANNOUNCE("NULL MOB!")]"
+ continue
entry += " - Playing as [C.mob.real_name]"
switch(C.mob.stat)
if(UNCONSCIOUS)
@@ -26,8 +32,8 @@
entry += " - DEAD"
var/age
- if(isnum(C.player_age))
- age = C.player_age
+ if(isnum(C.player.player_age))
+ age = C.player.player_age
else
age = 0
diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm
index 081541de2e7..ec72c40baeb 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -52,9 +52,9 @@ var/global/floorIsLava = 0
body += " \[Heal\] "
if(M.client)
- body += "
First connection: [M.client.player_age] days ago"
- body += "
BYOND account created: [M.client.account_join_date]"
- body += "
BYOND account age (days): [M.client.account_age]"
+ body += "
First connection: [M.client.player.player_age] days ago"
+ body += "
BYOND account created: [M.client.persistent.account_join]"
+ body += "
BYOND account age (days): [M.client.persistent.account_age]"
body += {"
\[
@@ -287,7 +287,7 @@ var/global/floorIsLava = 0
var/p_age = "unknown"
for(var/client/C in GLOB.clients)
if(C.ckey == key)
- p_age = C.player_age
+ p_age = C.player.player_age
break
dat +="Player age: [p_age]
"
diff --git a/code/modules/admin/admin_attack_log.dm b/code/modules/admin/admin_attack_log.dm
index 68c490dd02e..2ea66e73b9b 100644
--- a/code/modules/admin/admin_attack_log.dm
+++ b/code/modules/admin/admin_attack_log.dm
@@ -4,8 +4,8 @@
/mob/var/dialogue_log = list( )
/proc/log_and_message_admins(message as text, mob/user = usr)
- log_admin(user ? "[key_name(user)] [message]" : "EVENT [message]")
- message_admins(user ? "[key_name_admin(user)] [message]" : "EVENT [message]")
+ log_admin(user ? "[key_name(user)] [message]" : "[message]")
+ message_admins(user ? "[key_name_admin(user)] [message]" : "[message]")
/proc/log_and_message_admins_many(list/mob/users, message)
if(!users || !users.len)
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index a202f4fd41f..d4b9654bdbc 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -187,7 +187,6 @@ var/list/admin_verbs_server = list(
/client/proc/modify_server_news,
/client/proc/recipe_dump,
/client/proc/panicbunker,
- /client/proc/ip_reputation,
/client/proc/paranoia_logging,
/client/proc/reestablish_db_connection,
/client/proc/change_next_map,
diff --git a/code/modules/admin/verbs/panicbunker.dm b/code/modules/admin/verbs/panicbunker.dm
index 0a5155f2a3b..80b51f9d3d2 100644
--- a/code/modules/admin/verbs/panicbunker.dm
+++ b/code/modules/admin/verbs/panicbunker.dm
@@ -11,10 +11,13 @@ GLOBAL_LIST_EMPTY(bunker_passthrough)
to_chat(usr, "The Database is not enabled!")
return
- config_legacy.panic_bunker = (!config_legacy.panic_bunker)
+ var/now = CONFIG_GET(flag/panic_bunker)
+ now = !now
- log_and_message_admins("[key_name(usr)] has toggled the Panic Bunker, it is now [(config_legacy.panic_bunker?"on":"off")]")
- if (config_legacy.panic_bunker && (!SSdbcore.Connect()))
+ CONFIG_SET(flag/panic_bunker, now)
+
+ log_and_message_admins("[key_name(usr)] has toggled the Panic Bunker, it is now [now? "on" : "off"]")
+ if(now && (!SSdbcore.Connect()))
message_admins("The Database is not connected! Panic bunker will not work until the connection is reestablished.")
feedback_add_details("admin_verb","PANIC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
@@ -60,17 +63,3 @@ GLOBAL_LIST_EMPTY(bunker_passthrough)
if (config_legacy.paranoia_logging && (!SSdbcore.Connect()))
message_admins("The Database is not connected! Paranoia logging will not be able to give 'player age' (time since first connection) warnings, only Byond account warnings.")
feedback_add_details("admin_verb","PARLOG") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
-
-/client/proc/ip_reputation()
- set category = "Server"
- set name = "Toggle IP Rep Checks"
-
- if(!check_rights(R_ADMIN))
- return
-
- config_legacy.ip_reputation = (!config_legacy.ip_reputation)
-
- log_and_message_admins("[key_name(usr)] has toggled IP reputation checks, it is now [(config_legacy.ip_reputation?"on":"off")].")
- if (config_legacy.ip_reputation && (!SSdbcore.Connect()))
- message_admins("The database is not connected! IP reputation logging will not be able to allow existing players to bypass the reputation checks (if that is enabled).")
- feedback_add_details("admin_verb","IPREP") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm
index 45fe79d45df..4f4c8f04a8e 100644
--- a/code/modules/admin/verbs/randomverbs.dm
+++ b/code/modules/admin/verbs/randomverbs.dm
@@ -63,11 +63,11 @@
var/highlight_special_characters = 1
for(var/client/C in GLOB.clients)
- if(C.player_age == "Requires database")
+ if(C.player.player_age == "Requires database")
missing_ages = 1
continue
- if(C.player_age < age)
- msg += "[key_name(C, 1, 1, highlight_special_characters)]: account is [C.player_age] days old
"
+ if(C.player.player_age < age)
+ msg += "[key_name(C, 1, 1, highlight_special_characters)]: account is [C.player.player_age] days old
"
if(missing_ages)
to_chat(src, "Some accounts did not have proper ages set in their clients. This function requires database to be present.")
diff --git a/code/modules/client/client procs_vr.dm b/code/modules/client/client procs_vr.dm
deleted file mode 100644
index 60616a5db4b..00000000000
--- a/code/modules/client/client procs_vr.dm
+++ /dev/null
@@ -1,100 +0,0 @@
-//Uses a couple different services
-/client/update_ip_reputation()
- var/scores[] = list("GII" = ipr_getipintel(), "IPQS" = ipr_ipqualityscore())
-
- var/log_output = "IP Reputation [key] from [address]"
- var/worst = 0
-
- for(var/service in scores)
- var/score = scores[service]
- if(score > worst)
- worst = score
- log_output += " - [service] ([num2text(score)])"
-
- log_admin(log_output)
- ip_reputation = worst
- return TRUE
-
-//Service returns a single float in html body
-/client/proc/ipr_getipintel()
- if(!config_legacy.ipr_email)
- return -1
-
- var/request = "http://check.getipintel.net/check.php?ip=[address]&contact=[(config_legacy.ipr_email)]"
- var/http[] = world.Export(request)
-
- if(!http || !islist(http)) //If we couldn't check, the service might be down, fail-safe.
- log_admin("Couldn't connect to getipintel.net to check [address] for [key]")
- return -1
-
- //429 is rate limit exceeded
- if(text2num(http["STATUS"]) == 429)
- log_and_message_admins("getipintel.net reports HTTP status 429. IP reputation checking is now disabled. If you see this, let a developer know.")
- config_legacy.ip_reputation = FALSE
- return -1
-
- var/content = file2text(http["CONTENT"]) //world.Export actually returns a file object in CONTENT
- var/score = text2num(content)
- if(isnull(score))
- return -1
-
- //Error handling
- if(score < 0)
- var/fatal = TRUE
- var/ipr_error = "getipintel.net IP reputation check error while checking [address] for [key]: "
- switch(score)
- if(-1)
- ipr_error += "No input provided"
- if(-2)
- fatal = FALSE
- ipr_error += "Invalid IP provided"
- if(-3)
- fatal = FALSE
- ipr_error += "Unroutable/private IP (spoofing?)"
- if(-4)
- fatal = FALSE
- ipr_error += "Unable to reach database"
- if(-5)
- ipr_error += "Our IP is banned or otherwise forbidden"
- if(-6)
- ipr_error += "Missing contact info"
-
- log_and_message_admins(ipr_error)
- if(fatal)
- config_legacy.ip_reputation = FALSE
- log_and_message_admins("With this error, IP reputation checking is disabled for this shift. Let a developer know.")
- return -1
-
- //Went fine
- else
- return score
-
-//Service returns JSON in html body
-/client/proc/ipr_ipqualityscore()
- if(!config_legacy.ipqualityscore_apikey)
- return -1
-
- var/request = "http://www.ipqualityscore.com/api/json/ip/[(config_legacy.ipqualityscore_apikey)]/[address]?strictness=1&fast=true&byond_key=[key]"
- var/http[] = world.Export(request)
-
- if(!http || !islist(http)) //If we couldn't check, the service might be down, fail-safe.
- log_admin("Couldn't connect to ipqualityscore.com to check [address] for [key]")
- return -1
-
- var/content = file2text(http["CONTENT"]) //world.Export actually returns a file object in CONTENT
- var/response = json_decode(content)
- if(isnull(response))
- return -1
-
- //Error handling
- if(!response["success"])
- log_admin("IPQualityscore.com returned an error while processing [key] from [address]: " + response["message"])
- return -1
-
- var/score = 0
- if(response["proxy"])
- score = 100
- else
- score = response["fraud_score"]
-
- return score/100 //To normalize with the 0.0 to 1.0 scores.
diff --git a/code/modules/client/client.dm b/code/modules/client/client.dm
index 7c67e39d38c..915bb8a0680 100644
--- a/code/modules/client/client.dm
+++ b/code/modules/client/client.dm
@@ -44,6 +44,14 @@
/// Database data
var/datum/player_data/player
+ //? Connection
+ /// queued client security kick
+ var/queued_security_kick
+ /// currently age gate blocked
+ var/age_verification_open = FALSE
+ /// panic bunker is still resolving
+ var/panic_bunker_pending = FALSE
+
//? Rendering
/// Click catcher
var/atom/movable/screen/click_catcher/click_catcher
@@ -171,16 +179,6 @@
////////////////////////////////////
//things that require the database//
////////////////////////////////////
- ///So admins know why it isn't working - Used to determine how old the account is - in days.
- var/player_age = "(Requires database)"
- ///So admins know why it isn't working - Used to determine what other accounts previously logged in from this ip
- var/related_accounts_ip = "(Requires database)"
- ///So admins know why it isn't working - Used to determine what other accounts previously logged in from this computer id
- var/related_accounts_cid = "(Requires database)"
- ///Date that this account was first seen in the server
- var/account_join_date = "(Requires database)"
- ///Age of byond account in days
- var/account_age = "(Requires database)"
///Track hours of leave accured for each department.
var/list/department_hours = list()
@@ -213,3 +211,34 @@
/// If this client has been fully initialized or not
var/fully_created = FALSE
+
+/client/vv_edit_var(var_name, var_value)
+ switch (var_name)
+ if (NAMEOF(src, holder))
+ return FALSE
+ if (NAMEOF(src, ckey))
+ return FALSE
+ if (NAMEOF(src, key))
+ return FALSE
+ if(NAMEOF(src, view))
+ change_view(var_value, TRUE)
+ return TRUE
+ return ..()
+
+/**
+ * are we a guest account?
+ */
+/client/proc/is_guest()
+ return IsGuestKey(key)
+
+/**
+ * are we localhost?
+ */
+/client/proc/is_localhost()
+ return isnull(address) || (address in list("127.0.0.1", "::1"))
+
+/**
+ * are we any sort of staff rank?
+ */
+/client/proc/is_staff()
+ return !isnull(holder)
diff --git a/code/modules/client/client_data.dm b/code/modules/client/client_data.dm
index 5019309ac8d..b398216259b 100644
--- a/code/modules/client/client_data.dm
+++ b/code/modules/client/client_data.dm
@@ -13,15 +13,37 @@ GLOBAL_LIST_EMPTY(client_data)
* client data datums, to hold
* round-based data that we don't want wiped
* by a disconnect.
+ *
+ * this can absolutely contain player specific data, especially if we don't
+ * want to reload it every connect.
*/
/datum/client_data
/// owner ckey
var/ckey
/// absolutely, positively annihilated
var/ligma = FALSE
+ /// byond account join date
+ var/account_join
+ /// byond account age
+ var/account_age
+
+ //* externally managed data *//
+ /// playtime - role string to number of minutes.
+ var/list/playtime
+ /// playtime was loaded
+ var/playtime_loaded = FALSE
+ /// playtime is loading or flushing
+ var/playtime_mutex = FALSE
+ /// playtime - queued for addition
+ var/list/playtime_queued = list()
+ /// last REALTIMEOFDAY we did queuing
+ var/playtime_last
/datum/client_data/New(ckey)
src.ckey = ckey
+ src.playtime_last = REALTIMEOFDAY
+
+ load_account_age()
var/list/the_cheese_touch = CONFIG_GET(keyed_list/shadowban)
var/client/C = GLOB.directory[src.ckey]
@@ -38,3 +60,81 @@ GLOBAL_LIST_EMPTY(client_data)
if(src.ligma)
log_shadowban("[ckey] autobanned based on [why].")
message_admins(SPAN_DANGER("Automatically shadowbanning [ckey] based on configuration (matched on [why]). Varedit client.persistent.ligma to change this."))
+
+/datum/client_data/proc/load_playtime()
+ set waitfor = FALSE
+ if(playtime_loaded)
+ return
+ // no args, injection proof; release proccall guard
+ var/old_usr = usr
+ usr = null
+ load_playtime_impl()
+ usr = old_usr
+
+/datum/client_data/proc/load_playtime_impl()
+ PRIVATE_PROC(TRUE)
+ ASSERT(!playtime_mutex)
+ if(playtime_mutex)
+ return
+ playtime_mutex = TRUE
+ playtime = list()
+ var/player_id
+ var/client/client = GLOB.directory[ckey]
+ if(isnull(client))
+ playtime_mutex = FALSE
+ return
+ client.player.block_on_available()
+ // clients can be deleted at any time.
+ player_id = client?.player?.player_id
+ if(isnull(player_id))
+ playtime_mutex = FALSE
+ return
+ var/datum/db_query/query = SSdbcore.NewQuery(
+ "SELECT `roleid`, `minutes` FROM [format_table_name("playtime")] WHERE player = :player",
+ list(
+ "player" = player_id,
+ )
+ )
+ query.Execute()
+ while(query.NextRow())
+ playtime[query.item[1]] = text2num(query.item[2])
+ playtime_loaded = TRUE
+ playtime_mutex = FALSE
+
+/datum/client_data/proc/block_on_playtime_loaded(timeout = INFINITY)
+ var/timed_out = world.time + timeout
+ load_playtime()
+ UNTIL(playtime_loaded || world.time > timed_out)
+
+/datum/client_data/proc/block_on_account_age_loaded(timeout = INFINITY)
+ var/timed_out = world.time + timeout
+ UNTIL(!isnull(account_age) || world.time > timed_out)
+ return account_age
+
+/datum/client_data/proc/load_account_age()
+ var/list/http = world.Export("http://byond.com/members/[ckey]?format=text")
+ if(!http)
+ log_world("Failed to connect to byond age check for [ckey]")
+ return
+ var/F = file2text(http["CONTENT"])
+ . = null
+ if(F)
+ // year-month-day
+ var/regex/R = regex("joined = \"(\\d{4}-\\d{2}-\\d{2})\"")
+ if(R.Find(F))
+ var/str = R.group[1]
+ account_join = str
+ if(!SSdbcore.Connect())
+ account_age = null
+ return
+ var/datum/db_query/query = SSdbcore.RunQuery(
+ "SELECT DATEDIFF(Now(), :date)",
+ list(
+ "date" = str,
+ )
+ )
+ if(query.NextRow())
+ . = text2num(query.item[1])
+ else
+ CRASH("Age check regex failed for [src.ckey]")
+ account_age = .
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 1d0a0cd0512..b1ec9e1d256 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -5,12 +5,6 @@
///Could probably do with being lower.
///Restricts client uploads to the server to 1MB
#define UPLOAD_LIMIT 1048576
-GLOBAL_LIST_INIT(blacklisted_builds, list(
- "1407" = "bug preventing client display overrides from working leads to clients being able to see things/mobs they shouldn't be able to see",
- "1408" = "bug preventing client display overrides from working leads to clients being able to see things/mobs they shouldn't be able to see",
- "1428" = "bug causing right-click menus to show too many verbs that's been fixed in version 1429",
-
- ))
#define LIMITER_SIZE 5
#define CURRENT_SECOND 1
@@ -155,12 +149,13 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
return 1
+
///////////
//CONNECT//
///////////
/client/New(TopicData)
- //! pre-connect-ish
+ //* pre-connect-ish
// set appadmin for profiling or it might not work (?) (this is old code we just assume it's here for a reason)
world.SetConfig("APP/admin", ckey, "role=admin")
// block client.Topic() calls from connect
@@ -168,10 +163,8 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
// kick out invalid connections
if(connection != "seeker" && connection != "web")
return null
- // is localhost?
- var/is_localhost = isnull(address) || (address in list("127.0.0.1", "::1"))
// kick out guests
- if(!config_legacy.guests_allowed && IsGuestKey(key) && !is_localhost)
+ if(!config_legacy.guests_allowed && is_guest() && !is_localhost())
alert(src,"This server doesn't allow guest accounts to play. Please go to http://www.byond.com/ and register for a key.","Guest","OK")
del(src)
return
@@ -181,13 +174,25 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
GLOB.clients += src
GLOB.directory[ckey] = src
- //! Resolve storage datums
+ //* record their existence (tm)
+ // log & lookup updates
+ var/full_version = "[byond_version].[byond_build ? byond_build : "xxx"]"
+ // log connection in text file
+ log_access("Login: [key_name(src)] from [address ? address : "localhost"]-[computer_id] || BYOND v[full_version]")
+ // log to db
+ log_connection_to_db()
+ // log to player lookup
+ update_lookup_in_db()
+
+ //* Resolve storage datums
// resolve persistent data
persistent = resolve_client_data(ckey)
- // todo: move resolve database data up here but above preferences
+ //* Resolve database data
+ player = new(key)
+ player.log_connect()
// todo: move preferences up here but above persistent
- //! Setup user interface
+ //* Setup user interface
// todo: move top level menu here, for now it has to be under prefs.
// Instantiate statpanel
statpanel_boot()
@@ -196,16 +201,16 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
// Instantiate cutscene system
init_cutscene_system()
- //! Setup admin tooling
+ //* Setup admin tooling
GLOB.ahelp_tickets.ClientLogin(src)
- var/connecting_admin = FALSE //because de-admined admins connecting should be treated like admins.
+ // var/connecting_admin = FALSE //because de-admined admins connecting should be treated like admins.
//Admin Authorisation
holder = admin_datums[ckey]
var/debug_tools_allowed = FALSE
if(holder)
GLOB.admins |= src
holder.owner = src
- connecting_admin = TRUE
+ // connecting_admin = TRUE
//if(check_rights_for(src, R_DEBUG))
if(R_DEBUG & holder?.rights) //same wiht this, check_rights when?
debug_tools_allowed = TRUE
@@ -213,26 +218,15 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
else if(GLOB.deadmins[ckey])
add_verb(src, /client/proc/readmin)
connecting_admin = TRUE
- if(CONFIG_GET(flag/autoadmin))
- if(!GLOB.admin_datums[ckey])
- var/datum/admin_rank/autorank
- for(var/datum/admin_rank/R in GLOB.admin_ranks)
- if(R.name == CONFIG_GET(string/autoadmin_rank))
- autorank = R
- break
- if(!autorank)
- to_chat(world, "Autoadmin rank not found")
- else
- new /datum/admins(autorank, ckey)
*/
// if(CONFIG_GET(flag/enable_localhost_rank) && !connecting_admin)
- if(is_localhost)
+ if(is_localhost() && CONFIG_GET(flag/enable_localhost_rank))
holder = new /datum/admins("!localhost!", ALL, ckey)
holder.owner = src
GLOB.admins |= src
//admins |= src // this makes them not have admin. what the fuck??
// holder.associate(ckey)
- connecting_admin = TRUE
+ // connecting_admin = TRUE
//CITADEL EDIT
//if(check_rights_for(src, R_DEBUG)) //check if autoadmin gave us it
if(R_DEBUG & holder?.rights) //this is absolutely horrid
@@ -257,82 +251,28 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
// build top level menu
GLOB.main_window_menu.setup(src)
- var/full_version = "[byond_version].[byond_build ? byond_build : "xxx"]"
- log_access("Login: [key_name(src)] from [address ? address : "localhost"]-[computer_id] || BYOND v[full_version]")
- /*
- var/alert_mob_dupe_login = FALSE
- if(CONFIG_GET(flag/log_access))
- for(var/I in GLOB.clients)
- if(!I || I == src)
- continue
- var/client/C = I
- if(C.key && (C.key != key) )
- var/matches
- if( (C.address == address) )
- matches += "IP ([address])"
- if( (C.computer_id == computer_id) )
- if(matches)
- matches += " and "
- matches += "ID ([computer_id])"
- alert_mob_dupe_login = TRUE
- if(matches)
- if(C)
- message_admins("Notice: [key_name_admin(src)] has the same [matches] as [key_name_admin(C)].")
- log_admin_private("Notice: [key_name(src)] has the same [matches] as [key_name(C)].")
- else
- message_admins("Notice: [key_name_admin(src)] has the same [matches] as [key_name_admin(C)] (no longer logged in). ")
- log_admin_private("Notice: [key_name(src)] has the same [matches] as [key_name(C)] (no longer logged in).")
-
-
- */
-
- //! WARNING: mob.login is always called async, aka immediately returns on sleep.
- //! we cannot enforce nosleep due to SDMM limitations.
- //! therefore, DO NOT PUT ANYTHING YOU WILL RELY ON LATER IN THIS PROC IN LOGIN!
+ //* WARNING: mob.login is always called async, aka immediately returns on sleep.
+ //* we cannot enforce nosleep due to SDMM limitations.
+ //* therefore, DO NOT PUT ANYTHING YOU WILL RELY ON LATER IN THIS PROC IN LOGIN!
. = ..() //calls mob.Login()
- // if(!using_perspective)
- // stack_trace("mob login didn't put in perspective")
+ handle_legacy_connection_whatevers()
- if(log_client_to_db() == "BUNKER_DROPPED")
- disconnect_with_message("Disconnected by bunker: [config_legacy.panic_bunker_message]")
+ //* Connection Security
+ // start caching it immediately
+ INVOKE_ASYNC(SSipintel, TYPE_PROC_REF(/datum/controller/subsystem/ipintel, vpn_connection_check), address, ckey)
+ // run onboarding gauntlet
+ if(!onboarding())
+ if(!queued_security_kick)
+ security_kick("Unknown error during client init. Contact staff on Discord.", TRUE)
return FALSE
- // resolve database data
- // this is down here because player_lookup won't have an entry for us until log_client_to_db() runs!!
- player = new(ckey)
- player.log_connect()
-
- if (byond_version >= 512)
- if (!byond_build || byond_build < 1386)
- message_admins("[key_name(src)] has been detected as spoofing their byond version. Connection rejected.")
- add_system_note("Spoofed-Byond-Version", "Detected as using a spoofed byond version.")
- log_access("Failed Login: [key] - Spoofed byond version")
- qdel(src)
-
- if (num2text(byond_build) in GLOB.blacklisted_builds)
- log_access("Failed login: [key] - blacklisted byond version")
- to_chat(src, "Your version of byond is blacklisted.")
- to_chat(src, "Byond build [byond_build] ([byond_version].[byond_build]) has been blacklisted for the following reason: [GLOB.blacklisted_builds[num2text(byond_build)]].")
- to_chat(src, "Please download a new version of byond. If [byond_build] is the latest, you can go to BYOND's website to download other versions.")
- if(connecting_admin)
- to_chat(src, "As an admin, you are being allowed to continue using this version, but please consider changing byond versions")
- else
- disconnect_with_message("Your version of BYOND ([byond_version].[byond_build]) is blacklisted for the following reason: [GLOB.blacklisted_builds[num2text(byond_build)]]. Please download a new version of byond. If [byond_build] is the latest, you can go to BYOND's website to download other versions.")
- return
-
+ //* Initialize Input
if(SSinput.initialized)
set_macros()
update_movement_keys()
- // Initialize stat panel
- // stat_panel.initialize(
- // inline_html = file2text('html/statbrowser.html'),
- // inline_js = file2text('html/statbrowser.js'),
- // inline_css = file2text('html/statbrowser.css'),
- // )
-
- //! Initialize UI
+ //* Initialize UI
// initialize statbrowser
// (we don't, the JS does it for us. by signalling statpanel_ready().)
// Initialize tgui panel
@@ -348,34 +288,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
connection_realtime = world.realtime
connection_timeofday = world.timeofday
winset(src, null, "command=\".configure graphics-hwmode on\"")
- var/cev = CONFIG_GET(number/client_error_version)
- var/ceb = CONFIG_GET(number/client_error_build)
- var/cwv = CONFIG_GET(number/client_warn_version)
- if (byond_version < cev || (byond_version == cev && byond_build < ceb)) //Out of date client.
- to_chat(src, "Your version of BYOND is too old:")
- to_chat(src, CONFIG_GET(string/client_error_message))
- to_chat(src, "Your version: [byond_version].[byond_build]")
- to_chat(src, "Required version: [cev].[ceb] or later")
- to_chat(src, "Visit BYOND's website to get the latest version of BYOND.")
- if (connecting_admin)
- to_chat(src, "Because you are an admin, you are being allowed to walk past this limitation, But it is still STRONGLY suggested you upgrade")
- else
- disconnect_with_message("Your BYOND version ([byond_version].[byond_build]) is too old. Visit BYOND's website to get the latest version of BYOND.")
- return 0
- else if (byond_version < cwv) //We have words for this client.
- if(CONFIG_GET(flag/client_warn_popup))
- var/msg = "Your version of byond may be getting out of date:
"
- msg += CONFIG_GET(string/client_warn_message) + "
"
- msg += "Your version: [byond_version]
"
- msg += "Required version to remove this message: [cwv] or later
"
- msg += "Visit BYOND's website to get the latest version of BYOND.
"
- src << browse(msg, "window=warning_popup")
- else
- to_chat(src, "Your version of byond may be getting out of date:")
- to_chat(src, CONFIG_GET(string/client_warn_message))
- to_chat(src, "Your version: [byond_version]")
- to_chat(src, "Required version to remove this message: [cwv] or later")
- to_chat(src, "Visit BYOND's website to get the latest version of BYOND.")
/*
if (connection == "web" && !connecting_admin)
if (!CONFIG_GET(flag/allow_webclient))
@@ -421,14 +333,17 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
hook_vr("client_new",list(src))
if(config_legacy.paranoia_logging)
- if(isnum(player_age) && player_age == -1)
+ if(isnum(player.player_age) && player.player_age == -1)
log_and_message_admins("PARANOIA: [key_name(src)] has connected here for the first time.")
- if(isnum(account_age) && account_age <= 2)
- log_and_message_admins("PARANOIA: [key_name(src)] has a very new BYOND account ([account_age] days).")
+ if(isnum(persistent.account_age) && persistent.account_age <= 2)
+ log_and_message_admins("PARANOIA: [key_name(src)] has a very new BYOND account ([persistent.account_age] days).")
//? We are done
- // set initialized
- initialized = TRUE
+ // set initialized if we're not queued for a security kick
+ if(!queued_security_kick || panic_bunker_pending)
+ initialized = TRUE
+ else
+ addtimer(CALLBACK(src, PROC_REF(deferred_initialization_block)), 0)
// show any migration errors
prefs.auto_flush_errors()
// update our hub label
@@ -444,12 +359,18 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
return ..()
/client/Destroy()
+ // Unregister globals
GLOB.clients -= src
GLOB.directory -= ckey
+ // log
log_access("Logout: [key_name(src)]")
- GLOB.ahelp_tickets.ClientLogout(src)
+ // unreference storage datums
+ prefs = null
persistent = null
player = null
+
+ //* unsorted
+ GLOB.ahelp_tickets.ClientLogout(src)
if(prefs)
prefs.client = null
prefs = null
@@ -457,18 +378,21 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
if(holder)
holder.owner = null
GLOB.admins -= src //delete them on the managed one too
- if(using_perspective)
- set_perspective(null)
active_mousedown_item = null
SSping.currentrun -= src
- //! cleanup UI
- /// cleanup statbrowser
+ //* cleanup mob-side stuff
+ // clear perspective
+ if(using_perspective)
+ set_perspective(null)
+
+ //* cleanup UI
+ // cleanup statbrowser
statpanel_dispose()
- /// cleanup cutscene system
+ // cleanup cutscene system
cleanup_cutscene_system()
- /// cleanup tgui panel
+ // cleanup tgui panel
QDEL_NULL(tgui_panel)
. = ..() //Even though we're going to be hard deleted there are still some things that want to know the destroy is happening
@@ -503,156 +427,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
create_message("note", key, system_ckey, message, null, null, 0, 0, null, 0, 0)
*/
-// Returns null if no DB connection can be established, or -1 if the requested key was not found in the database
-
-/client/proc/log_client_to_db()
-
- if ( IsGuestKey(src.key) )
- return
-
- if(!SSdbcore.Connect())
- return
-
- var/sql_ckey = sql_sanitize_text(src.ckey)
-
- var/datum/db_query/query = SSdbcore.RunQuery(
- "SELECT id, datediff(Now(), firstseen) as age FROM [format_table_name("player_lookup")] WHERE ckey = :ckey",
- list(
- "ckey" = sql_ckey
- )
- )
- var/sql_id = 0
- player_age = -1 // New players won't have an entry so knowing we have a connection we set this to zero to be updated if their is a record.
- while(query.NextRow())
- sql_id = query.item[1]
- player_age = text2num(query.item[2])
- break
-
- account_join_date = sanitizeSQL(findJoinDate())
- if(account_join_date && SSdbcore.Connect())
- var/datum/db_query/query_datediff = SSdbcore.RunQuery(
- "SELECT DATEDIFF(Now(), :date)",
- list(
- "date" = account_join_date
- )
- )
- if(query_datediff.NextRow())
- account_age = text2num(query_datediff.item[1])
-
- var/datum/db_query/query_ip = SSdbcore.RunQuery(
- "SELECT ckey FROM [format_table_name("player_lookup")] WHERE ip = :addr",
- list(
- "addr" = address
- )
- )
- related_accounts_ip = ""
- while(query_ip.NextRow())
- related_accounts_ip += "[query_ip.item[1]], "
- break
-
- var/datum/db_query/query_cid = SSdbcore.RunQuery(
- "SELECT ckey FROM [format_table_name("player_lookup")] WHERE computerid = :cid",
- list(
- "cid" = sanitizeSQL(computer_id)
- )
- )
- related_accounts_cid = ""
- while(query_cid.NextRow())
- related_accounts_cid += "[query_cid.item[1]], "
- break
-
- //Just the standard check to see if it's actually a number
- if(sql_id)
- if(istext(sql_id))
- sql_id = text2num(sql_id)
- if(!isnum(sql_id))
- return
-
- var/admin_rank = "Player"
- if(src.holder)
- admin_rank = src.holder.rank
-
- var/sql_ip = sql_sanitize_text(src.address) || "0.0.0.0"
- var/sql_computerid = sql_sanitize_text(src.computer_id)
- var/sql_admin_rank = sql_sanitize_text(admin_rank)
-
- //Panic bunker code
- if ((player_age == -1) && !(ckey in GLOB.bunker_passthrough)) //first connection
- if (config_legacy.panic_bunker && !holder && !deadmin_holder)
- log_adminwarn("Failed Login: [key] - New account attempting to connect during panic bunker")
- message_admins("Failed Login: [key] - New account attempting to connect during panic bunker")
- to_chat(src, config_legacy.panic_bunker_message)
- return "BUNKER_DROPPED"
- if(player_age == -1)
- player_age = 0 //math requires this to not be -1.
-
- if(config_legacy.ip_reputation)
- if(config_legacy.ipr_allow_existing && player_age >= config_legacy.ipr_minimum_age)
- log_admin("Skipping IP reputation check on [key] with [address] because of player age")
- else if(update_ip_reputation()) //It is set now
- if(ip_reputation >= config_legacy.ipr_bad_score) //It's bad
- //Log it
- if(config_legacy.paranoia_logging) //We don't block, but we want paranoia log messages
- log_and_message_admins("[key] at [address] has bad IP reputation: [ip_reputation]. Will be kicked if enabled in config.")
- else //We just log it
- log_admin("[key] at [address] has bad IP reputation: [ip_reputation]. Will be kicked if enabled in config.")
-
- //Take action if required
- if(config_legacy.ipr_block_bad_ips && config_legacy.ipr_allow_existing) //We allow players of an age, but you don't meet it
- disconnect_with_message("Sorry, we only allow VPN/Proxy/Tor usage for players who have spent at least [config_legacy.ipr_minimum_age] days on the server. If you are unable to use the internet without your VPN/Proxy/Tor, please contact an admin out-of-game to let them know so we can accommodate this.")
- return 0
- else if(config_legacy.ipr_block_bad_ips) //We don't allow players of any particular age
- disconnect_with_message("Sorry, we do not accept connections from users via VPN/Proxy/Tor connections. If you believe this is in error, contact an admin out-of-game.")
- return 0
- else
- log_admin("Couldn't perform IP check on [key] with [address]")
-
- // Department Hours
- if(config_legacy.time_off)
- var/datum/db_query/query_hours = SSdbcore.RunQuery(
- "SELECT department, hours FROM [format_table_name("vr_player_hours")] WHERE ckey = :ckey",
- list(
- "ckey" = sql_ckey
- )
- )
- while(query_hours.NextRow())
- LAZYINITLIST(department_hours)
- department_hours[query_hours.item[1]] = text2num(query_hours.item[2])
-
- if(sql_id)
- SSdbcore.RunQuery(
- "UPDATE [format_table_name("player_lookup")] SET lastseen = Now(), ip = :ip, computerid = :computerid, lastadminrank = :lastadminrank WHERE id = :id",
- list(
- "ip" = sql_ip,
- "computerid" = sql_computerid,
- "lastadminrank" = sql_admin_rank,
- "id" = sql_id
- )
- )
- else
- //New player!! Need to insert all the stuff
- SSdbcore.RunQuery(
- "INSERT INTO [format_table_name("player_lookup")] (id, ckey, firstseen, lastseen, ip, computerid, lastadminrank) VALUES (null, :ckey, Now(), Now(), :ip, :cid, :rank)",
- list(
- "ckey" = sql_ckey,
- "ip" = sql_ip,
- "cid" = sql_computerid,
- "rank" = sql_admin_rank
- )
- )
-
- //Logging player access
- var/serverip = "[world.internet_address]:[world.port]"
- SSdbcore.RunQuery(
- "INSERT INTO [format_table_name("connection_log")] (id, datetime, serverip, ckey, ip, computerid) VALUES (null, Now(), :serverip, :ckey, :ip, :computerid)",
- list(
- "serverip" = serverip,
- "ckey" = sql_ckey,
- "ip" = sql_ip,
- "computerid" = sql_computerid
- )
- )
-
#undef UPLOAD_LIMIT
//checks if a client is afk
@@ -673,7 +447,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
else
stoplag(5)
-
/client/Click(atom/object, atom/location, control, params)
var/ab = FALSE
var/list/L = params2list(params)
@@ -768,81 +541,6 @@ GLOBAL_VAR_INIT(log_clicks, FALSE)
/client/proc/setDir(newdir)
dir = newdir
-/client/vv_edit_var(var_name, var_value)
- switch (var_name)
- if (NAMEOF(src, holder))
- return FALSE
- if (NAMEOF(src, ckey))
- return FALSE
- if (NAMEOF(src, key))
- return FALSE
- if(NAMEOF(src, view))
- change_view(var_value, TRUE)
- return TRUE
- . = ..()
-
-/client/proc/change_view(new_size, forced, translocate)
- set waitfor = FALSE // to async temporary view
- // todo: refactor this, client view changes should be ephemeral.
- var/list/L = decode_view_size(new_size)
- set_temporary_view(L[1], L[2])
-
-/**
- * directly sets our view
- * you should probably be using perspective datums most of the time instead
- * WARNING: this is verbatim; aka, view = 7 is 15 width 15 height, NOT 7x7!
- *
- * furthermore, this proc is BLOCKING.
- */
-/client/proc/set_temporary_view(width, height)
- if(!width || !height || width < 0 || height < 0)
- reset_temporary_view()
- return
- using_temporary_viewsize = TRUE
- // round up; even views are illegal.
- if(!(width % 2))
- width++
- if(!(height % 2))
- height++
- temporary_viewsize_width = width
- temporary_viewsize_height = height
- request_viewport_update()
-
-/**
- * resets our temporary view
- * you should probably be using perspective datums most of the time instead
- *
- * furthermore, this proc is BLOCKING
- */
-/client/proc/reset_temporary_view()
- using_temporary_viewsize = FALSE
- temporary_viewsize_height = null
- temporary_viewsize_width = null
- request_viewport_update()
-
-/**
- * switch perspective - null will cause us to shunt our eye to nullspace!
- */
-/client/proc/set_perspective(datum/perspective/P)
- if(using_perspective)
- using_perspective.remove_client(src, TRUE)
- if(using_perspective)
- stack_trace("using perspective didn't clear")
- using_perspective = null
- if(!P)
- eye = null
- lazy_eye = 0
- perspective = EYE_PERSPECTIVE
- return
- P.add_client(src)
- if(using_perspective != P)
- stack_trace("using perspective didn't set")
-
-/**
- * reset perspective to default - usually to our mob's
- */
-/client/proc/reset_perspective()
- set_perspective(mob.get_perspective())
/mob/proc/MayRespawn()
return 0
@@ -854,91 +552,6 @@ GLOBAL_VAR_INIT(log_clicks, FALSE)
// Something went wrong, client is usually kicked or transfered to a new mob at this point
return 0
-/client/verb/character_setup()
- set name = "Character Setup"
- set category = "Preferences"
- if(prefs)
- prefs.ShowChoices(usr)
-
-/client/proc/findJoinDate()
- var/list/http = world.Export("http://byond.com/members/[ckey]?format=text")
- if(!http)
- log_world("Failed to connect to byond age check for [ckey]")
- return
- var/F = file2text(http["CONTENT"])
- if(F)
- var/regex/R = regex("joined = \"(\\d{4}-\\d{2}-\\d{2})\"")
- if(R.Find(F))
- . = R.group[1]
- else
- CRASH("Age check regex failed for [src.ckey]")
-
/client/proc/AnnouncePR(announcement)
- //if(prefs && prefs.chat_toggles & CHAT_PULLR)
to_chat(src, announcement)
-//This is for getipintel.net.
-//You're welcome to replace this proc with your own that does your own cool stuff.
-//Just set the client's ip_reputation var and make sure it makes sense with your config settings (higher numbers are worse results)
-/client/proc/update_ip_reputation()
- var/request = "http://check.getipintel.net/check.php?ip=[address]&contact=[config_legacy.ipr_email]"
- var/http[] = world.Export(request)
-
- /* Debug
- TO_WORLD_log("Requested this: [request]")
- for(var/entry in http)
- TO_WORLD_log("[entry] : [http[entry]]")
- */
-
- if(!http || !islist(http)) //If we couldn't check, the service might be down, fail-safe.
- log_admin("Couldn't connect to getipintel.net to check [address] for [key]")
- return FALSE
-
- //429 is rate limit exceeded
- if(text2num(http["STATUS"]) == 429)
- log_and_message_admins("getipintel.net reports HTTP status 429. IP reputation checking is now disabled. If you see this, let a developer know.")
- config_legacy.ip_reputation = FALSE
- return FALSE
-
- var/content = file2text(http["CONTENT"]) //world.Export actually returns a file object in CONTENT
- var/score = text2num(content)
- if(isnull(score))
- return FALSE
-
- //Error handling
- if(score < 0)
- var/fatal = TRUE
- var/ipr_error = "getipintel.net IP reputation check error while checking [address] for [key]: "
- switch(score)
- if(-1)
- ipr_error += "No input provided"
- if(-2)
- fatal = FALSE
- ipr_error += "Invalid IP provided"
- if(-3)
- fatal = FALSE
- ipr_error += "Unroutable/private IP (spoofing?)"
- if(-4)
- fatal = FALSE
- ipr_error += "Unable to reach database"
- if(-5)
- ipr_error += "Our IP is banned or otherwise forbidden"
- if(-6)
- ipr_error += "Missing contact info"
-
- log_and_message_admins(ipr_error)
- if(fatal)
- config_legacy.ip_reputation = FALSE
- log_and_message_admins("With this error, IP reputation checking is disabled for this shift. Let a developer know.")
- return FALSE
-
- //Went fine
- else
- ip_reputation = score
- return TRUE
-
-/client/proc/disconnect_with_message(var/message = "You have been intentionally disconnected by the server.
This may be for security or administrative reasons.")
- message = "