From 78a633ba801d67e5b04cd17f045ab62bd38f5e59 Mon Sep 17 00:00:00 2001 From: silicons <2003111+silicons@users.noreply.github.com> Date: Mon, 26 Jun 2023 12:43:50 -0700 Subject: [PATCH] new client security and refactors and playtimes and--- (#5603) cleans up config adds new role time.tracking system adds new panic bunker adds new VPN detection system unfucks the automated age gate does a bunch more cleanups refactors client security checks a little --------- Co-authored-by: VM_USER Co-authored-by: silicons --- SQL/database_schema.sql | 50 ++ SQL/database_schema_prefixed.sql | 52 +- citadel.dme | 26 +- code/__DEFINES/admin/bans.dm | 1 - code/__DEFINES/client/player_flags.dm | 8 +- code/__DEFINES/client/playtime.dm | 11 + code/__DEFINES/controllers/_subsystems.dm | 1 + code/__DEFINES/preferences/savefiles.dm | 2 +- code/__HELPERS/_logging.dm | 6 + code/__HELPERS/time.dm | 3 + .../configuration/entries/admin.dm | 1 + .../entries/{bot.dm => chat_bridge.dm} | 0 .../configuration/entries/compile.dm | 2 - .../entries/{comms.dm => cross_server.dm} | 2 + .../entries/{dbconfig.dm => database.dm} | 0 .../configuration/entries/fail2topic.dm | 19 - .../entries/{game_options.dm => game.dm} | 0 .../configuration/entries/general.dm | 18 +- .../configuration/entries/lobby.dm | 9 + .../configuration/entries/logging.dm | 89 --- .../configuration/entries/playtime.dm | 8 + .../configuration/entries/security.dm | 79 +++ .../configuration_old/configuration.dm | 42 -- code/controllers/subsystem/input.dm | 5 +- code/controllers/subsystem/ipintel.dm | 217 ++++++++ .../subsystem/persistence/bunker.dm | 2 +- code/controllers/subsystem/playtime.dm | 85 +++ code/controllers/subsystem/tgui.dm | 1 + code/datums/world_topic.dm | 64 --- code/game/antagonist/antagonist.dm | 6 +- code/game/verbs/advanced_who.dm | 4 +- code/game/verbs/who.dm | 10 +- code/modules/admin/admin.dm | 8 +- code/modules/admin/admin_attack_log.dm | 4 +- code/modules/admin/admin_verbs.dm | 1 - code/modules/admin/verbs/panicbunker.dm | 23 +- code/modules/admin/verbs/randomverbs.dm | 6 +- code/modules/client/client procs_vr.dm | 100 ---- code/modules/client/client.dm | 49 +- code/modules/client/client_data.dm | 100 ++++ code/modules/client/client_procs.dm | 507 +++--------------- code/modules/client/connection.dm | 81 +++ code/modules/client/legacy.dm | 15 + code/modules/client/onboarding/_onboarding.dm | 13 + .../client/onboarding/age_verification.dm | 108 ++++ .../modules/client/onboarding/panic_bunker.dm | 48 ++ .../client/onboarding/security_checks.dm | 47 ++ code/modules/client/perspective.dm | 23 + code/modules/client/player_data.dm | 23 +- code/modules/client/security.dm | 58 ++ code/modules/client/statpanel.dm | 7 + code/modules/client/verbs/ooc.dm | 10 + .../client/verbs/panic_bunker_player.dm | 1 - code/modules/client/verbs/preferences.dm | 11 + code/modules/client/viewport.dm | 39 ++ code/modules/ghostroles/role.dm | 2 - code/modules/jobs/job.dm | 8 +- code/modules/mob/death.dm | 1 + code/modules/mob/health.dm | 2 + code/modules/mob/mob.dm | 7 +- code/modules/mob/new_player/new_player.dm | 158 +----- code/modules/preferences/migration.dm | 20 + .../preference_setup/global/setting_datums.dm | 8 - .../preferences/preferences_toggle_procs.dm | 16 - code/modules/roles/role.dm | 3 + config/access levels.txt | 79 --- config/config.toml | 16 - config/config.txt | 65 +-- config/entries/admin.txt | 2 + config/entries/{bot.txt => chat_bridge.txt} | 0 config/entries/cross_server.txt | 15 + config/entries/{dbconfig.txt => database.txt} | 6 +- config/entries/fail2topic.txt | 18 - config/entries/{game_options.txt => game.txt} | 23 + config/entries/general.txt | 48 ++ config/entries/hub.txt | 12 - config/entries/lobby.txt | 13 + config/entries/logging.txt | 5 + config/entries/playtime.txt | 7 + config/{ => entries}/resources.txt | 0 config/entries/security.txt | 54 ++ config/entries/urls.txt | 3 + .../tgui/interfaces/AgeVerifyMenu.tsx | 129 +++++ 83 files changed, 1636 insertions(+), 1189 deletions(-) create mode 100644 code/__DEFINES/client/playtime.dm create mode 100644 code/controllers/configuration/entries/admin.dm rename code/controllers/configuration/entries/{bot.dm => chat_bridge.dm} (100%) delete mode 100644 code/controllers/configuration/entries/compile.dm rename code/controllers/configuration/entries/{comms.dm => cross_server.dm} (91%) rename code/controllers/configuration/entries/{dbconfig.dm => database.dm} (100%) delete mode 100644 code/controllers/configuration/entries/fail2topic.dm rename code/controllers/configuration/entries/{game_options.dm => game.dm} (100%) create mode 100644 code/controllers/configuration/entries/playtime.dm create mode 100644 code/controllers/configuration/entries/security.dm create mode 100644 code/controllers/subsystem/ipintel.dm create mode 100644 code/controllers/subsystem/playtime.dm delete mode 100644 code/modules/client/client procs_vr.dm create mode 100644 code/modules/client/connection.dm create mode 100644 code/modules/client/legacy.dm create mode 100644 code/modules/client/onboarding/_onboarding.dm create mode 100644 code/modules/client/onboarding/age_verification.dm create mode 100644 code/modules/client/onboarding/panic_bunker.dm create mode 100644 code/modules/client/onboarding/security_checks.dm create mode 100644 code/modules/client/perspective.dm create mode 100644 code/modules/client/security.dm create mode 100644 code/modules/client/verbs/preferences.dm delete mode 100644 config/access levels.txt delete mode 100644 config/config.toml create mode 100644 config/entries/admin.txt rename config/entries/{bot.txt => chat_bridge.txt} (100%) create mode 100644 config/entries/cross_server.txt rename config/entries/{dbconfig.txt => database.txt} (88%) delete mode 100644 config/entries/fail2topic.txt rename config/entries/{game_options.txt => game.txt} (52%) create mode 100644 config/entries/general.txt delete mode 100644 config/entries/hub.txt create mode 100644 config/entries/logging.txt create mode 100644 config/entries/playtime.txt rename config/{ => entries}/resources.txt (100%) create mode 100644 config/entries/security.txt create mode 100644 tgui/packages/tgui/interfaces/AgeVerifyMenu.tsx 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 = "You Have Been Disconnected
[message]


If you feel this is in error, you can contact an administrator out-of-game (for example, on Discord)." - window_flash(src) - src << browse(message,"window=dropmessage;size=480x360;can_close=1") - qdel(src) diff --git a/code/modules/client/connection.dm b/code/modules/client/connection.dm new file mode 100644 index 00000000000..5e45f624d36 --- /dev/null +++ b/code/modules/client/connection.dm @@ -0,0 +1,81 @@ +/client/proc/update_lookup_in_db() + // ensure db is up + if(!SSdbcore.Connect()) + return + // unlike connection log, we don't care about guest keys here + if(is_guest()) + return + + var/datum/db_query/lookup = SSdbcore.NewQuery( + "SELECT id FROM [format_table_name("player_lookup")] WHERE ckey = :ckey", + list( + "ckey" = ckey, + ) + ) + lookup.Execute() + var/sql_id + if(lookup.NextRow()) + sql_id = text2num(lookup.item[1]) + qdel(lookup) + + if(sql_id) + var/datum/db_query/update = SSdbcore.NewQuery( + "UPDATE [format_table_name("player_lookup")] SET lastseen = Now(), ip = :ip, computerid = :computerid, lastadminrank = :lastadminrank WHERE id = :id", + list( + "ip" = address, + "computerid" = computer_id, + "lastadminrank" = holder?.rank || "Player", + "id" = sql_id, + ) + ) + update.Execute() + qdel(update) + else + //New player!! Need to insert all the stuff + var/datum/db_query/insert = SSdbcore.NewQuery( + "INSERT INTO [format_table_name("player_lookup")] (id, ckey, firstseen, lastseen, ip, computerid, lastadminrank) VALUES (null, :ckey, Now(), Now(), :ip, :cid, :rank)", + list( + "ckey" = ckey, + "ip" = address, + "cid" = computer_id, + "rank" = holder?.rank || "Player", + ) + ) + insert.Execute() + qdel(insert) + +/client/proc/log_connection_to_db() + set waitfor = FALSE + log_connection_to_db_impl() + +/client/proc/log_connection_to_db_impl() + PRIVATE_PROC(TRUE) + + if(!SSdbcore.Connect()) + return + + SSdbcore.RunQuery( + "INSERT INTO [format_table_name("connection_log")] (id, datetime, serverip, ckey, ip, computerid) VALUES (null, Now(), :server, :ckey, :ip, :cid)", + list( + "server" = "[world.internet_address]:[world.port]", + "ckey" = ckey, + "ip" = address || "0.0.0.0", + "cid" = computer_id, + ) + ) + +/client/proc/reject_on_initialization_block() + if(!initialized) + to_chat(src, SPAN_DANGER("Your client is still initializing. Wait a moment.")) + return FALSE + return TRUE + +/** + * initializes us once everything resolves + * this is necessary because things like IPIntel/panic bunker stuff + * take time to resolve, and we don't want to block client/New(). + */ +/client/proc/deferred_initialization_block() + UNTIL(!panic_bunker_pending) + UNTIL(!queued_security_kick) + initialized = TRUE diff --git a/code/modules/client/legacy.dm b/code/modules/client/legacy.dm new file mode 100644 index 00000000000..0a5de478051 --- /dev/null +++ b/code/modules/client/legacy.dm @@ -0,0 +1,15 @@ +/client/proc/handle_legacy_connection_whatevers() + if(is_guest()) + return + + // 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" = ckey + ) + ) + while(query_hours.NextRow()) + LAZYINITLIST(department_hours) + department_hours[query_hours.item[1]] = text2num(query_hours.item[2]) diff --git a/code/modules/client/onboarding/_onboarding.dm b/code/modules/client/onboarding/_onboarding.dm new file mode 100644 index 00000000000..5fa68709b1a --- /dev/null +++ b/code/modules/client/onboarding/_onboarding.dm @@ -0,0 +1,13 @@ +/** + * runs the gauntlet of checks we use on join + * + * client should already be logged to DB at this point. + */ +/client/proc/onboarding() + if(!security_checks()) + return FALSE + if(!panic_bunker()) + return FALSE + if(!age_verification()) + return FALSE + return TRUE diff --git a/code/modules/client/onboarding/age_verification.dm b/code/modules/client/onboarding/age_verification.dm new file mode 100644 index 00000000000..44c43bf13c5 --- /dev/null +++ b/code/modules/client/onboarding/age_verification.dm @@ -0,0 +1,108 @@ +/client/proc/age_verification() + if(!player.block_on_available(10 SECONDS)) + message_admins("FATAL: player data unavailable during age verification.") + . = TRUE + CRASH("player data was not available during age verification") + if(is_staff()) + set_age_verified() + return TRUE + if(is_age_verified()) + return TRUE + GLOB.age_verify_menu.ui_interact(mob) + return TRUE + +/client/proc/set_age_verified() + if(player.player_flags & PLAYER_FLAG_AGE_VERIFIED) + return TRUE + player.player_flags |= PLAYER_FLAG_AGE_VERIFIED + player.save() + return TRUE + +/client/proc/is_age_verified() + SHOULD_NOT_SLEEP(TRUE) + return !CONFIG_GET(flag/age_verification) || (player.player_flags & PLAYER_FLAG_AGE_VERIFIED) || is_staff() + +/client/proc/reject_age_unverified() + SHOULD_NOT_SLEEP(TRUE) + . = is_age_verified() + if(!.) + to_chat(src, SPAN_DANGER("Age verification must be completed before doing that.")) + +GLOBAL_DATUM_INIT(age_verify_menu, /datum/age_verify_menu, new) + +/datum/age_verify_menu + +/datum/age_verify_menu/ui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "AgeVerifyMenu") + ui.open() + +/datum/age_verify_menu/ui_status(mob/user, datum/ui_state/state, datum/tgui_module/module) + return UI_INTERACTIVE + +/datum/age_verify_menu/ui_act(action, list/params, datum/tgui/ui) + . = ..() + if(.) + return + + if(usr.client.is_age_verified()) + return TRUE + + var/player_month = text2num(params["month"]) + var/player_year = text2num(params["year"]) + + if(isnull(player_month) || isnull(player_year)) + return TRUE + + var/current_time = world.realtime + var/current_month = text2num(time2text(current_time, "MM")) + var/current_year = text2num(time2text(current_time, "YYYY")) + + var/player_total_months = (player_year * 12) + player_month + + var/current_total_months = (current_year * 12) + current_month + + var/months_in_eighteen_years = 18 * 12 + + var/month_difference = current_total_months - player_total_months + if(month_difference > months_in_eighteen_years) + usr.client.age_gate_internal_succeeded() + else + if(month_difference < months_in_eighteen_years) + usr.client.age_gate_internal_failed() + else + //they could be 17 or 18 depending on the /day/ they were born in + var/current_day = text2num(time2text(current_time, "DD")) + var/days_in_months = list(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + if((player_year % 4) == 0) // leap year so february actually has 29 days + days_in_months[2] = 29 + var/total_days_in_player_month = days_in_months[player_month] + var/list/days = list() + for(var/number in 1 to total_days_in_player_month) + days += number + var/player_day = text2num(params["day"]) + if(isnull(player_day)) + return TRUE + if(player_day <= current_day) + //their birthday has passed + usr.client.age_gate_internal_succeeded() + else + //it has NOT been their 18th birthday yet + usr.client.age_gate_internal_failed() + return TRUE + +/client/proc/age_gate_internal_failed() + var/datum/tgui/found = SStgui.get_open_ui(usr, GLOB.age_verify_menu) + if(!isnull(found)) + found.close() + if(CONFIG_GET(flag/age_verification_autoban)) + security_ban("Age verification failed. Appeal this on the Discord after you are 18 years of age or older.") + else + security_kick("Age verification failed. This server is for 18+ only.", TRUE) + +/client/proc/age_gate_internal_succeeded() + var/datum/tgui/found = SStgui.get_open_ui(usr, GLOB.age_verify_menu) + if(!isnull(found)) + found.close() + set_age_verified() diff --git a/code/modules/client/onboarding/panic_bunker.dm b/code/modules/client/onboarding/panic_bunker.dm new file mode 100644 index 00000000000..98bb3a7ba63 --- /dev/null +++ b/code/modules/client/onboarding/panic_bunker.dm @@ -0,0 +1,48 @@ +/** + * Panic bunker is special: We can defer the check until IPIntel comes back. + */ +/client/proc/panic_bunker() + set waitfor = FALSE + . = TRUE + panic_bunker_pending = TRUE + run_bunker_checks() + +/client/proc/run_bunker_checks() + // we need database up + if(!SSdbcore.Connect()) + panic_bunker_pending = FALSE + return + var/allowed = blocking_bunker_checks() + if(allowed) + panic_bunker_pending = FALSE + player.block_on_available() + if(!(player.player_flags & PLAYER_FLAG_CONSIDERED_SEEN)) + player.player_flags |= PLAYER_FLAG_CONSIDERED_SEEN + player.save() + else + disconnection_message("[CONFIG_GET(string/panic_bunker_message)]") + security_kick("new account kicked by panic bunker") + +/client/proc/blocking_bunker_checks() + // is bunker on? + if(!CONFIG_GET(flag/panic_bunker) && (!CONFIG_GET(flag/vpn_bunker) || !SSipintel.vpn_check(address))) + return TRUE + // allow bypass + if(ckey in GLOB.bunker_passthrough) + return TRUE + + // at time of writing this is in minutes + var/needed_playtime = CONFIG_GET(number/panic_bunker_playtime) + if(!needed_playtime) + // using connection mode + // block on player flags + player.block_on_available() + if(player.player_flags & PLAYER_FLAG_CONSIDERED_SEEN) + return TRUE + return FALSE + else + // using hour mode + // block on player playtime + persistent.block_on_playtime_loaded() + // we use living playtime + return persistent.playtime[PLAYER_PLAYTIME_LIVING] >= needed_playtime diff --git a/code/modules/client/onboarding/security_checks.dm b/code/modules/client/onboarding/security_checks.dm new file mode 100644 index 00000000000..fbf15c4e16c --- /dev/null +++ b/code/modules/client/onboarding/security_checks.dm @@ -0,0 +1,47 @@ +GLOBAL_LIST_INIT(blacklisted_builds, list( + "1407" = "a bug preventing client display overrides from working leads to clients being able to see things/mobs they shouldn't be able to see", + "1408" = "a bug preventing client display overrides from working leads to clients being able to see things/mobs they shouldn't be able to see", + "1428" = "a bug causing right-click menus to show too many verbs that's been fixed in version 1429", + )) + +/client/proc/security_checks() + if(byond_version < 513) + security_kick("BYOND 512 and prior clients are too outdated.", tell_user = TRUE) + return FALSE + if(!byond_build || byond_build < 1386) + message_admins("[src] detected as BYOND version spoof. Kicking.") + security_note("likely spoofed byond version") + security_kick("likely spoofed byond version") + return FALSE + if(num2text(byond_build, 999) in GLOB.blacklisted_builds) + security_kick("[byond_build] is blacklisted due to [GLOB.blacklisted_builds[num2text(byond_build)]]. Please update your BYOND version.", tell_user = TRUE) + return FALSE + 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 (is_staff()) + 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 + disconnection_message("Your BYOND version ([byond_version].[byond_build]) is too old. Visit BYOND's website to get the latest version of BYOND.") + return FALSE + 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.") + return TRUE diff --git a/code/modules/client/perspective.dm b/code/modules/client/perspective.dm new file mode 100644 index 00000000000..7e2f72d21d2 --- /dev/null +++ b/code/modules/client/perspective.dm @@ -0,0 +1,23 @@ +/** + * 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()) diff --git a/code/modules/client/player_data.dm b/code/modules/client/player_data.dm index d2ed3d8f9ee..e88b833efed 100644 --- a/code/modules/client/player_data.dm +++ b/code/modules/client/player_data.dm @@ -6,6 +6,8 @@ //! intrinsics /// our ckey var/ckey + /// our key + var/key /// available: null if don't know yet, FALSE if no dbcon, TRUE if loaded var/available /// loading? @@ -22,11 +24,14 @@ var/player_flags = NONE /// player age var/player_age + /// join date + var/player_first_seen -/datum/player_data/New(ckey) - src.ckey = ckey +/datum/player_data/New(key) + src.ckey = ckey(key) if(!src.ckey) return + src.key = key load() /** @@ -62,6 +67,8 @@ /datum/player_data/proc/_load() if(IsAdminAdvancedProcCall()) return + if(IsGuestKey(key)) + return var/datum/db_query/lookup lookup = SSdbcore.ExecuteQuery( "SELECT id, playerid, firstseen FROM [format_table_name("player_lookup")] WHERE ckey = :ckey", @@ -81,7 +88,7 @@ lookup_pid = text2num(lookup_pid) qdel(lookup) lookup = SSdbcore.ExecuteQuery( - "SELECT id, flags, datediff(Now(), firstseen) FROM [format_table_name("player")] WHERE id = :id", + "SELECT id, flags, datediff(Now(), firstseen), firstseen FROM [format_table_name("player")] WHERE id = :id", list( "id" = lookup_pid ) @@ -96,6 +103,7 @@ lookup_age = text2num(lookup_age) player_id = lookup_pid player_flags = lookup_flags + player_first_seen = lookup.item[4] player_age = lookup_age qdel(lookup) available = TRUE @@ -120,6 +128,7 @@ "fs" = migrate_firstseen ) ) + player_first_seen = migrate_firstseen else insert = SSdbcore.ExecuteQuery( "INSERT INTO [format_table_name("player")] (flags, firstseen, lastseen) VALUES (:flags, Now(), Now())", @@ -127,6 +136,7 @@ "flags" = player_flags, ) ) + player_first_seen = time_stamp() var/insert_id = insert.last_insert_id if(istext(insert_id)) insert_id = text2num(insert_id) @@ -206,9 +216,12 @@ /** * block until we know if we're available * then return if we are + * + * WARNING: without database, or if this is for a guest key, we will never be available. */ -/datum/player_data/proc/block_on_available() - UNTIL(!isnull(available)) +/datum/player_data/proc/block_on_available(timeout = INFINITY) + var/timed_out = world.time + timeout + UNTIL(!isnull(available) || world.time > timed_out) return available /** diff --git a/code/modules/client/security.dm b/code/modules/client/security.dm new file mode 100644 index 00000000000..b5506b80ed8 --- /dev/null +++ b/code/modules/client/security.dm @@ -0,0 +1,58 @@ +/client/proc/security_note(message, tell_user) + log_access("client security: noting [key_name(src)] | [message]") + message_admins("client security: noting [key_name(src)] for [message]") + add_system_note("client-security", message) + if(tell_user) + to_chat(src, SPAN_BOLDANNOUNCE("CLIENT-SECURITY: [message]
Please correct this.")) + +/client/proc/security_kick(message, tell_user, immediate) + log_access("client security: kicking [key_name(src)] | [message]") + message_admins("client security: kicking [key_name(src)] | [message]") + if(tell_user) + to_chat(src, SPAN_BOLDANNOUNCE("CLIENT-SECURITY: [message]
Please correct this.
You will now be disconnected.")) + disconnection_message(message) + if(!immediate) + queue_security_kick(5 SECONDS) + queue_security_kick() + +/** + * time is in minutes + * tell_user is implicit + */ +/client/proc/security_ban(message, time = -1) + var/time_displayed = time == -1? "" : "for [DisplayTimeText(time MINUTES)]" + log_access("client security: banning [key_name(src)] [time == -1? "" : "for [time_displayed]"] | [message]") + message_admins("client security: banning [key_name(src)] [time == -1? "" : "for [time_displayed]"] | [message]") + add_system_note("client-security", "banned for [time_displayed]: [message]") + qdel(src) + AddBan(ckey, computer_id, "client-security: [message]", minutes = time) + +/** + * queues a security kick + * ensures the client's around for atleast delay or after current proc chain is over + */ +/client/proc/queue_security_kick(delay) + var/left = isnull(queued_security_kick)? null : timeleft(queued_security_kick) + if(isnull(left) || left < delay) + deltimer(queued_security_kick) + addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(qdel), src), delay, TIMER_STOPPABLE) + +/** + * shows a disconnection message that's hopefully resistant to fast-disconnects + */ +/client/proc/disconnection_message(msg) + var/content = {" + + + Disconnection + + + You have been intentionally disconnected by the server.

+
[msg]
+

+ If you feel this is in error, contact an administrator out of game (e.g. on Discord). + + + "} + src << browse(content, "window=droppedFromServer;size=480x360;can_close=1") + window_flash(src) diff --git a/code/modules/client/statpanel.dm b/code/modules/client/statpanel.dm index b3bde695716..f26ce397ebf 100644 --- a/code/modules/client/statpanel.dm +++ b/code/modules/client/statpanel.dm @@ -25,6 +25,13 @@ * boots statpanel up during connect */ /client/proc/statpanel_boot() + set waitfor = FALSE + statpanel_boot_impl() + +/client/proc/statpanel_boot_impl() + PRIVATE_PROC(TRUE) + // give client a second to load + sleep(2 SECONDS) // loads statbrowser if it isn't there src << browse(file('html/statbrowser.html'), "window=statbrowser") // if it is there and we can't tell because byond is byond, send it a signal to reload diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm index edebbca5fef..578b34a6c56 100644 --- a/code/modules/client/verbs/ooc.dm +++ b/code/modules/client/verbs/ooc.dm @@ -47,6 +47,11 @@ set name = "OOC" //Gave this shit a shorter name so you only have to time out "ooc" rather than "ooc message" to use it --NeoFite set category = "OOC" + if(!reject_on_initialization_block()) + return + if(!reject_age_unverified()) + return + if(IsGuestKey(key)) to_chat(src, "Guests may not use OOC.") return @@ -147,6 +152,11 @@ set desc = "Local OOC, seen only by those in view." set category = "OOC" + if(!reject_on_initialization_block()) + return + if(!reject_age_unverified()) + return + if(!mob) return diff --git a/code/modules/client/verbs/panic_bunker_player.dm b/code/modules/client/verbs/panic_bunker_player.dm index d59f4a3e37f..7fba73bda80 100644 --- a/code/modules/client/verbs/panic_bunker_player.dm +++ b/code/modules/client/verbs/panic_bunker_player.dm @@ -6,7 +6,6 @@ world.send_cross_server_bunker_overrides(key, src) /world/proc/send_cross_server_bunker_overrides(key, client/C) - if(!C.is_preference_enabled(/datum/client_preference/debug/age_verified)) return var/comms_key = CONFIG_GET(string/comms_key) if(!comms_key) return diff --git a/code/modules/client/verbs/preferences.dm b/code/modules/client/verbs/preferences.dm new file mode 100644 index 00000000000..2dbdcf1370e --- /dev/null +++ b/code/modules/client/verbs/preferences.dm @@ -0,0 +1,11 @@ +/client/verb/character_setup() + set name = "Character Setup" + set category = "Preferences" + + prefs?.ShowChoices(usr) + +/client/verb/preferences() + set name = "Game Options" + set category = "Preferences" + + prefs?.ShowChoices(usr) diff --git a/code/modules/client/viewport.dm b/code/modules/client/viewport.dm index d9af3ffe09b..45c66610b00 100644 --- a/code/modules/client/viewport.dm +++ b/code/modules/client/viewport.dm @@ -5,6 +5,45 @@ * This is becaues CENTER-50% or CENTER-7 alignment systems don't work with even numbers. */ +/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() + // these two variables control max dynamic resize for viewport GLOBAL_VAR_INIT(max_client_view_x, 19) GLOBAL_VAR_INIT(max_client_view_y, 15) diff --git a/code/modules/ghostroles/role.dm b/code/modules/ghostroles/role.dm index a5006789f46..cd30afebca8 100644 --- a/code/modules/ghostroles/role.dm +++ b/code/modules/ghostroles/role.dm @@ -48,8 +48,6 @@ GLOBAL_LIST_INIT(ghostroles, init_ghostroles()) var/important_info /// should we show the standard ghostrole greeting? var/show_standard_greeting = TRUE - /// snowflake ID for if we're not to be referred to by path - dynamically created ghostolres - var/id /// spawnerless - advanced users only. This isn't for "load in spawners in PreInstantiate()", this is for true spawnpoint-less ghostroles. var/spawnerless = FALSE /// assigned role. defaults to name. diff --git a/code/modules/jobs/job.dm b/code/modules/jobs/job.dm index b25fb1a4357..d610599d869 100644 --- a/code/modules/jobs/job.dm +++ b/code/modules/jobs/job.dm @@ -3,8 +3,6 @@ abstract_type = /datum/role/job //? Intrinsics - /// ID of the job, used for save/load - var/id /// The name of the job , used for preferences, bans and more. Make sure you know what you're doing before changing this. var/title = "NOPE" /// Description of the job @@ -375,8 +373,10 @@ /datum/role/job/proc/available_in_days(client/C) if(C.has_jexp_bypass()) return 0 - if(C && config_legacy.use_age_restriction_for_jobs && isnum(C.player_age) && isnum(minimal_player_age)) - return max(0, minimal_player_age - C.player_age) + if(!CONFIG_GET(flag/job_check_account_age)) + return 0 + if(isnum(C.player.player_age) && isnum(minimal_player_age)) + return max(0, minimal_player_age - C.player.player_age) return 0 /datum/role/job/proc/apply_fingerprints(var/mob/living/carbon/human/target) diff --git a/code/modules/mob/death.dm b/code/modules/mob/death.dm index f5089e89e33..a0d07ca2daa 100644 --- a/code/modules/mob/death.dm +++ b/code/modules/mob/death.dm @@ -66,6 +66,7 @@ if(!gibbed && deathmessage != "no message") // This is gross, but reliable. Only brains use it. visible_message("[src] [deathmessage]") + SSplaytime.queue_playtimes(client) set_stat(DEAD) update_ssd_overlay() diff --git a/code/modules/mob/health.dm b/code/modules/mob/health.dm index d1ff50acc24..80fefae3fde 100644 --- a/code/modules/mob/health.dm +++ b/code/modules/mob/health.dm @@ -62,6 +62,8 @@ // full heal if requested if(full_heal) rejuvenate(TRUE) + // flush playtimes + SSplaytime.queue_playtimes(client) // set to conscious set_stat(CONSCIOUS) // immediately update to kick down if needed diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 1f9a95163a6..4bca27547f5 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -559,6 +559,9 @@ set category = "OOC" set desc = "Return to the lobby." + // don't lose out on that sweet observer playtime + SSplaytime.queue_playtimes(client) + if(stat != DEAD) to_chat(usr, SPAN_BOLDNOTICE("You must be dead to use this!")) return @@ -611,9 +614,7 @@ set name = "Observe" set category = "OOC" - if(!client.is_preference_enabled(/datum/client_preference/debug/age_verified)) - return - else if(stat != DEAD || istype(src, /mob/new_player)) + if(stat != DEAD || istype(src, /mob/new_player)) to_chat(usr, "You must be observing to use this!") return diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm index 9b15f0e4954..107b16dd470 100644 --- a/code/modules/mob/new_player/new_player.dm +++ b/code/modules/mob/new_player/new_player.dm @@ -3,9 +3,7 @@ var/spawning = 0 // Referenced when you want to delete the new_player later on in the code. var/totalPlayers = 0 // Player counts for the Lobby tab var/totalPlayersReady = 0 - var/show_hidden_jobs = 0 // Show jobs that are set to "Never" in preferences var/datum/browser/panel - var/age_gate_result universal_speak = 1 invisibility = 101 @@ -34,10 +32,6 @@ new_player_panel_proc() /mob/new_player/proc/new_player_panel_proc() - if(age_gate_result == null && client.prefs && !client.is_preference_enabled(/datum/client_preference/debug/age_verified)) // run first time verification - verifyage() - if(!client) - return // verifyage sleeps what the fuck var/output = "
" output +="
" output += "

Character Setup

" @@ -87,74 +81,6 @@ panel.open() return -/mob/new_player/proc/age_gate() - var/list/dat = list("
") - dat += "Enter your date of birth here, to confirm that you are over 18.
" - dat += "Your date of birth is not saved, only the fact that you are over/under 18 is.
" - dat += "
" - - dat += "
" - dat += "" - dat += "" - dat += "" - dat += "
" - dat += "
" - - winshow(src, "age_gate", TRUE) - var/datum/browser/popup = new(src, "age_gate", "
Age Gate
", 400, 250) - popup.set_window_options("can_close=0") - popup.set_content(dat.Join()) - popup.open(FALSE) - onclose(src, "age_gate") - - while(age_gate_result == null) - stoplag(1) - - popup.close() - - return age_gate_result - -/mob/new_player/proc/verifyage() - UNTIL(client.prefs.initialized) // fuck this stupid ass broken piece of shit age gate - if(client.holder) // they're an admin - client.set_preference(/datum/client_preference/debug/age_verified, 1) - return TRUE - if(!client.is_preference_enabled(/datum/client_preference/debug/age_verified)) //make sure they are verified - if(!client.prefs) - message_admins("Blocked [src] from new player panel because age gate could not access client preferences.") - return FALSE - else - var/hasverified = client.is_preference_enabled(/datum/client_preference/debug/age_verified) - if(!hasverified) //they have not completed age gate - var/verify = age_gate() - if(verify == FALSE) - client.add_system_note("Automated-Age-Gate", "Failed automatic age gate process") - //ban them and kick them - to_chat(src, "You have failed the initial age verification check. \nIf you believe this was in error, you MUST submit to additional verification on the forums at citadel-station.net/forum/") - if(client) - AddBan(ckey, computer_id, "Failed initial age verification check. Appeal at citadel-station.net/forum/", "SYSTEM", 0, 0) - Logout() - return FALSE - else - //they claim to be of age, so allow them to continue and update their flags - client.set_preference(/datum/client_preference/debug/age_verified, 1) - SScharacters.queue_preferences_save(client.prefs) - //log this - message_admins("[ckey] has joined through the automated age gate process.") - return TRUE - return TRUE - - /mob/new_player/statpanel_data(client/C) . = ..() @@ -188,58 +114,19 @@ if(src != usr) return 0 - if(!client) - return 0 - - //don't let people get to this unless they are specifically not verified - if(href_list["Month"] && !client.is_preference_enabled(/datum/client_preference/debug/age_verified)) - var/player_month = text2num(href_list["Month"]) - var/player_year = text2num(href_list["Year"]) - - var/current_time = world.realtime - var/current_month = text2num(time2text(current_time, "MM")) - var/current_year = text2num(time2text(current_time, "YYYY")) - - var/player_total_months = (player_year * 12) + player_month - - var/current_total_months = (current_year * 12) + current_month - - var/months_in_eighteen_years = 18 * 12 - - var/month_difference = current_total_months - player_total_months - if(month_difference > months_in_eighteen_years) - age_gate_result = TRUE // they're fine - else - if(month_difference < months_in_eighteen_years) - age_gate_result = FALSE - else - //they could be 17 or 18 depending on the /day/ they were born in - var/current_day = text2num(time2text(current_time, "DD")) - var/days_in_months = list(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - if((player_year % 4) == 0) // leap year so february actually has 29 days - days_in_months[2] = 29 - var/total_days_in_player_month = days_in_months[player_month] - var/list/days = list() - for(var/number in 1 to total_days_in_player_month) - days += number - var/player_day = input(src, "What day of [player_month] were you born in.") as anything in days - if(player_day <= current_day) - //their birthday has passed - age_gate_result = TRUE - else - //it has NOT been their 18th birthday yet - age_gate_result = FALSE - - if(!verifyage()) - return - - if(!client) return 0 - if(href_list["show_preferences"]) + if(!client.reject_age_unverified()) + return + if(!client.reject_on_initialization_block()) + return client.prefs.ShowChoices(src) return 1 if(href_list["ready"]) + if(!client.reject_age_unverified()) + return + if(!client.reject_on_initialization_block()) + return if(!SSticker || SSticker.current_state <= GAME_STATE_PREGAME) // Make sure we don't ready up after the round has started var/want_to_be_ready = text2num(href_list["ready"]) if(want_to_be_ready) @@ -267,7 +154,12 @@ new_player_panel_proc() if(href_list["observe"]) - if(!client.is_preference_enabled(/datum/client_preference/debug/age_verified)) return + // don't lose out if we join fast + SSplaytime.queue_playtimes(client) + if(!client.reject_age_unverified()) + return + if(!client.reject_on_initialization_block()) + return var/alert_time = SSticker?.current_state <= GAME_STATE_SETTING_UP ? 1 : round(config_legacy.respawn_time/10/60) if(alert(src,"Are you sure you wish to observe? You will have to wait up to [alert_time] minute\s before being able to spawn into the game!","Player Setup","Yes","No") == "Yes") @@ -309,6 +201,10 @@ return 1 if(href_list["late_join"]) + if(!client.reject_age_unverified()) + return + if(!client.reject_on_initialization_block()) + return if(!SSticker || SSticker.current_state != GAME_STATE_PLAYING) to_chat(usr, "The round is either not ready, or has already finished...") @@ -330,9 +226,15 @@ LateChoices() if(href_list["manifest"]) + if(!client.reject_age_unverified()) + return + ViewManifest() if(href_list["privacy_poll"]) + if(!client.reject_age_unverified()) + return + if(!SSdbcore.Connect()) return var/voted = 0 @@ -442,10 +344,6 @@ handle_server_news() return - if(href_list["hidden_jobs"]) - show_hidden_jobs = !show_hidden_jobs - LateChoices() - /mob/new_player/proc/handle_server_news() if(!client) return @@ -484,8 +382,8 @@ return timer - world.time /mob/new_player/proc/AttemptLateSpawn(rank) - if(!client.is_preference_enabled(/datum/client_preference/debug/age_verified)) - return + // don't lose out if we join fast + SSplaytime.queue_playtimes(client) if (src != usr) return 0 if(SSticker.current_state != GAME_STATE_PLAYING) @@ -592,8 +490,8 @@ /mob/new_player/proc/create_character(var/turf/T) - if(!client.is_preference_enabled(/datum/client_preference/debug/age_verified)) - return FALSE + // don't lose out if we join fast + SSplaytime.queue_playtimes(client) if(!spawn_checks_vr()) return FALSE var/list/errors = list() diff --git a/code/modules/preferences/migration.dm b/code/modules/preferences/migration.dm index 8d064704dde..dd2476f315c 100644 --- a/code/modules/preferences/migration.dm +++ b/code/modules/preferences/migration.dm @@ -19,6 +19,26 @@ if(!islist(language_prefixes)) language_prefixes = list() options[GLOBAL_DATA_LANGUAGE_PREFIX] = language_prefixes.Copy() + if(current_version < 16) + if(prefs.client) + var/list/pref_datum_entries + S["preferences"] >> pref_datum_entries + var/was_age_verified = ("AGE_VERIFIED" in pref_datum_entries) + if(was_age_verified) + var/datum/player_data/data = prefs?.client?.player + // alright well, this is evil, but whatever + // yes, this is a blocking proc + // this can do horrible things but hey, fuck the old age gate system and whoever allowed it to stand. + if(!isnull(data) && (SSdbcore.Connect() && data.block_on_available())) + if(data.player_flags & PLAYER_FLAG_AGE_VERIFIED) + else + data.player_flags |= PLAYER_FLAG_AGE_VERIFIED + var/datum/tgui/open_verify = SStgui.get_open_ui(prefs.client.mob, GLOB.age_verify_menu) + if(!isnull(open_verify)) + qdel(open_verify) + INVOKE_ASYNC(data, TYPE_PROC_REF(/datum/player_data, save)) + else + log_and_message_admins("Failed to automatically authorize age gating for player with savefile [prefs.path]") /** * @params diff --git a/code/modules/preferences/preference_setup/global/setting_datums.dm b/code/modules/preferences/preference_setup/global/setting_datums.dm index 4d1b21b9824..e08681ee23d 100644 --- a/code/modules/preferences/preference_setup/global/setting_datums.dm +++ b/code/modules/preferences/preference_setup/global/setting_datums.dm @@ -352,14 +352,6 @@ var/list/_client_preferences_by_type enabled_description = "Obfuscate Ghost" disabled_description = "Normal Ghost" - -/datum/client_preference/debug/age_verified - description = "(Debug) Age Verified Status" - key = "AGE_VERIFIED" - enabled_description = "TRUE" - disabled_description = "FALSE" - enabled_by_default = FALSE - /datum/client_preference/autocorrect description = "Autocorrect" key = "AUTOCORRECT" diff --git a/code/modules/preferences/preferences_toggle_procs.dm b/code/modules/preferences/preferences_toggle_procs.dm index 90b8fdd76a3..edc0c067e19 100644 --- a/code/modules/preferences/preferences_toggle_procs.dm +++ b/code/modules/preferences/preferences_toggle_procs.dm @@ -338,22 +338,6 @@ feedback_add_details("admin_verb","TBeSpecial") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -/client/proc/toggle_age_verification() - set name = "Toggle age verification status" - set category = "Debug" - set desc = "Toggles your age verified status." - - var/pref_path = /datum/client_preference/debug/age_verified - - toggle_preference(pref_path) - - to_chat(src,"You will [ (is_preference_enabled(pref_path)) ? "now" : "no longer"] be prompted to verify age.") - - SScharacters.queue_preferences_save(prefs) - - feedback_add_details("admin_verb","TAgeVerify") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - /mob/living/carbon/human/verb/toggle_pain_msg() set name = "Toggle Pain Messages" set category = "Preferences" diff --git a/code/modules/roles/role.dm b/code/modules/roles/role.dm index af9a40bc7f8..e1c13059141 100644 --- a/code/modules/roles/role.dm +++ b/code/modules/roles/role.dm @@ -4,3 +4,6 @@ */ /datum/role abstract_type = /datum/role + + /// unique id + var/id diff --git a/config/access levels.txt b/config/access levels.txt deleted file mode 100644 index e6c0519069a..00000000000 --- a/config/access levels.txt +++ /dev/null @@ -1,79 +0,0 @@ -HOW TO CONVERT A MAP TO THE NEW (june 2008) ACCESS LEVEL SYSTEM -1. Open the .dmp file up in Notepad -2. Find all the "access = blahblah" attributes of doors. -3. Delete them. -4. Open the map up in Dream Maker. If you didn't get them all, it'll tell you so. -5. Assign the existing doors new access permissions using the method below. - -HOW TO MAKE A MAP USING THE NEW (june 2008) ACCESS LEVEL SYSTEM -1. Make a map as normal -2. Select a door that you want to not be accessible to everybody -3. Right click on it and edit its attributes -4. Make the "req_access_txt" attribute be a semicolon-separated list of the permissions required to open the doors -5. Repeat for all doors. - -For example, a brig door would have it be "2" while a door that requires you have toxins and teleporter access (for whatever reason) would have it be "9;20" - -Here is a list of the permissions and their numbers (this may be out of date, see code/game/access.dm for an updated version): - -access_security = 1 -access_brig = 2 -access_armory = 3 -access_forensics_lockers= 4 -access_medical = 5 -access_morgue = 6 -access_tox = 7 -access_tox_storage = 8 -access_genetics = 9 -access_engine = 10 -access_engine_equip= 11 -access_maint_tunnels = 12 -access_external_airlocks = 13 -access_emergency_storage = 14 -access_change_ids = 15 -access_ai_upload = 16 -access_teleporter = 17 -access_eva = 18 -access_heads = 19 -access_captain = 20 -access_all_personal_lockers = 21 -access_chapel_office = 22 -access_tech_storage = 23 -access_atmospherics = 24 -access_bar = 25 -access_janitor = 26 -access_crematorium = 27 -access_kitchen = 28 -access_robotics = 29 -access_rd = 30 -access_cargo = 31 -access_construction = 32 -access_chemistry = 33 -access_cargo_bot = 34 -access_hydroponics = 35 -access_manufacturing = 36 -access_library = 37 -access_lawyer = 38 -access_virology = 39 -access_cmo = 40 -access_qm = 41 -access_court = 42 -access_clown = 43 -access_mime = 44 -access_surgery = 45 -access_theatre = 46 -access_research = 47 -access_mining = 48 -access_mining_office = 49 //not in use -access_mailsorting = 50 -access_mint = 51 -access_mint_vault = 52 -access_heads_vault = 53 -access_mining_station = 54 -access_xenobiology = 55 -access_ce = 56 -access_hop = 57 -access_hos = 58 -access_RC_announce = 59 //Request console announcements -access_keycard_auth = 60 //Used for events which require at least two people to confirm them -access_tcomsat = 61 // has access to the entire telecomms satellite / machinery \ No newline at end of file diff --git a/config/config.toml b/config/config.toml deleted file mode 100644 index 096401a321b..00000000000 --- a/config/config.toml +++ /dev/null @@ -1,16 +0,0 @@ -### TOML CONFIGURATION SYSTEM -# -# Adapted from paradise station -# -# # denotes a comment -# [section] to mark a section -# otherwise, just list values. -# -### - -########################################################## - -[photography] - -# - diff --git a/config/config.txt b/config/config.txt index 9dd54be0747..da816676953 100644 --- a/config/config.txt +++ b/config/config.txt @@ -1,63 +1,16 @@ # You can use the "$include" directive to split your configs however you want -#$include dbconfig.txt -#$include comms.txt -#$include antag_rep.txt -$include resources.txt -$include logging.txt -$include entries/dbconfig.txt -$include entries/fail2topic.txt -$include entries/game_options.txt +$include entries/admin.txt +$include entries/chat_bridge.txt +$include entries/cross_server.txt +$include entries/database.txt +$include entries/game.txt +$include entries/general.txt $include entries/health.txt -$include entries/hub.txt $include entries/lobby.txt +$include entries/logging.txt $include entries/photography.txt +$include entries/resources.txt +$include entries/security.txt $include entries/skills.txt $include entries/urls.txt - - -# You can use the @ character at the beginning of a config option to lock it from being edited in-game -# Example usage: -# @SERVERNAME CitadelRP -# Which sets the SERVERNAME, and disallows admins from being able to change it using View Variables. -# @LOG_TWITTER 0 -# Which explicitly disables LOG_TWITTER, as well as locking it. -# There are various options which are hard-locked for security reasons. - -## Uncomment to set the number of /world/Reboot()s before the DreamDaemon restarts itself. 0 means restart every round. Requires tgstation server tools. -#ROUNDS_UNTIL_HARD_RESTART 5 - -## Uncomment to enable minimap generation -MINIMAPS_ENABLED - -## System command that invokes youtube-dl, used by Play Internet Sound. -## You can install youtube-dl with -## "pip install youtube-dl" if you have pip installed -## from https://github.com/rg3/youtube-dl/releases -## or your package manager -## The default value assumes youtube-dl is in your system PATH -# INVOKE_YOUTUBEDL youtube-dl - -## CLIENT VERSION CONTROL -## This allows you to configure the minimum required client version, as well as a warning version, and message for both. -## These trigger for any version below (non-inclusive) the given version, so 510 triggers on 509 or lower. -## These messages will be followed by one stating the clients current version and the required version for clarity. -## If CLIENT_WARN_POPUP is uncommented a popup window with the message will be displayed instead -#CLIENT_WARN_VERSION 511 -#CLIENT_WARN_POPUP -#CLIENT_WARN_MESSAGE Byond released 511 as the stable release. You can set the framerate your client runs at, which makes the game feel very different and cool. Shortly after its release we will end up using 511 client features and you will be forced to update. -CLIENT_ERROR_VERSION 511 -CLIENT_ERROR_MESSAGE Your version of byond is not supported. Please upgrade. -## The minimum build needed for joining the server, if using 512, a good minimum build would be 1421 as that disables the Middle Mouse Button exploit. -CLIENT_ERROR_BUILD 1421 - -## Assets can opt-in to caching their results into `tmp`. -## This is important, as preferences assets take upwards of 30 seconds (without sleeps) to collect. -## The cache is assumed to be cleared by TGS recompiling, which deletes `tmp`. -## This should be disabled (through `CACHE_ASSETS 0`) on development, -## but enabled on production (the default). -CACHE_ASSETS 0 - -## Comment to disable sending a toast notification on the host server when initializations complete. -## Even if this is enabled, a notification will only be sent if there are no clients connected. -TOAST_NOTIFICATION_ON_INIT diff --git a/config/entries/admin.txt b/config/entries/admin.txt new file mode 100644 index 00000000000..af60991a94c --- /dev/null +++ b/config/entries/admin.txt @@ -0,0 +1,2 @@ +## Enable giving host admin rank to localhost connections +ENABLE_LOCALHOST_RANK diff --git a/config/entries/bot.txt b/config/entries/chat_bridge.txt similarity index 100% rename from config/entries/bot.txt rename to config/entries/chat_bridge.txt diff --git a/config/entries/cross_server.txt b/config/entries/cross_server.txt new file mode 100644 index 00000000000..ef0d152eaa0 --- /dev/null +++ b/config/entries/cross_server.txt @@ -0,0 +1,15 @@ +## Cross-Server Comms ## + +## Our secret key - used to auth incoming messages +# COMMS_KEY default_pwd + +## allow sending out cross server bunker overrides +## TODO: remove / replace +ALLOW_CROSS_SERVER_BUNKER_OVERRIDE + +## List of cross server bunker overrides +## TODO: remove / replace +# CROSS_SERVER_BUNKER_OVERRIDE + +## Our server name in cross-server communications +# CROSS_COMM_NAME Citadel Station 13 - RP diff --git a/config/entries/dbconfig.txt b/config/entries/database.txt similarity index 88% rename from config/entries/dbconfig.txt rename to config/entries/database.txt index 6537f53b1cb..8608646a50e 100644 --- a/config/entries/dbconfig.txt +++ b/config/entries/database.txt @@ -1,6 +1,6 @@ ## Enables SQL/database usage ## WARNING: A lot of the codebase requires SQL. Not using this is a terrible idea. -#SQL_ENABLED +# SQL_ENABLED ## SQL address SQL_ADDRESS localhost @@ -35,5 +35,5 @@ BLOCKING_QUERY_TIMEOUT 5 ## The maximum number of additional threads BSQL is allowed to run at once BSQL_THREAD_LIMIT 50 -## Uncomment to enable verbose BSQL communication logs -#BSQL_DEBUG +## Time to wait before considering a query as lingering too long +QUERY_DEBUG_LOG_TIMEOUT 70 diff --git a/config/entries/fail2topic.txt b/config/entries/fail2topic.txt deleted file mode 100644 index f46341ae8fd..00000000000 --- a/config/entries/fail2topic.txt +++ /dev/null @@ -1,18 +0,0 @@ -### FAIL2TOPIC: -### Automated IP bans for world/Topic() spammers -## Enabled -FAIL2TOPIC_ENABLED -## Minimum wait time in deciseconds between valid requests -FAIL2TOPIC_RATE_LIMIT 10 -## Number of requests after breaching rate limit that triggers a ban -FAIL2TOPIC_MAX_FAILS 5 -## Firewall rule name used on physical server -FAIL2TOPIC_RULE_NAME _dd_fail2topic - -## HARD topic limit, even without fail2topic -TOPIC_MAX_SIZE 1048576 - -### FAIL2TOPIC WHITELISTING -## Just list IPs like so: -# TOPIC_RATE_LIMIT_WHITELIST 351.531.632.145 obviously not valid but you get the point -TOPIC_RATE_LIMIT_WHITELIST 127.0.0.1 diff --git a/config/entries/game_options.txt b/config/entries/game.txt similarity index 52% rename from config/entries/game_options.txt rename to config/entries/game.txt index 53df494473d..f969403248c 100644 --- a/config/entries/game_options.txt +++ b/config/entries/game.txt @@ -5,9 +5,15 @@ MAX_VIEWPORT_SIZE 19x15 ## AT THE CURRENT TIME, BYOND WORLD.VIEW IS READ-ONLY ## THIS CONFIGURATION IS LEFT HERE AS A TODO. +## Set to 0 to disable starlight +STARLIGHT + ## Set to 0 to disable holidays (you monster) ALLOW_HOLIDAYS +## Set to 0 to disable nightshifts (you monster!!) +NIGHTSHIFTS_ENABLED + ## Engine submap probabilities ## Supermatter ENGINE_SUBMAP EngineSubmap_SM 40 @@ -17,3 +23,20 @@ ENGINE_SUBMAP EngineSubmap_RUST 40 ENGINE_SUBMAP EngineSubmap_Tesla 20 ## Singulo ENGINE_SUBMAP EngineSubmap_Singulo 20 + +## Alert Levels +# ALERT_DESC_GREEN TEXT HERE +# ALERT_DESC_BLUE_UPTO TEXT HERE +# ALERT_DESC_BLUE_DOWNTO TEXT HERE +# ALERT_DESC_YELLOW_UPTO TEXT HERE +# ALERT_DESC_YELLOW_DOWNTO TEXT HERE +# ALERT_DESC_VIOLET_UPTO TEXT HERE +# ALERT_DESC_VIOLET_DOWNTO TEXT HERE +# ALERT_DESC_ORANGE_UPTO TEXT HERE +# ALERT_DESC_ORANGE_DOWNTO TEXT HERE +# ALERT_DESC_RED_UPTO TEXT HERE +# ALERT_DESC_RED_DOWNTO TEXT HERE +# ALERT_DESC_DELTA TEXT HERE + +## Set to 0 to disable emoji parse +EMOJIS diff --git a/config/entries/general.txt b/config/entries/general.txt new file mode 100644 index 00000000000..a74cb64b0f7 --- /dev/null +++ b/config/entries/general.txt @@ -0,0 +1,48 @@ +# You can use the @ character at the beginning of a config option to lock it from being edited in-game +# Example usage: +# @SERVERNAME CitadelRP +# Which sets the SERVERNAME, and disallows admins from being able to change it using View Variables. +# @LOG_TWITTER 0 +# Which explicitly disables LOG_TWITTER, as well as locking it. +# There are various options which are hard-locked for security reasons. + +## Uncomment to enable minimap generation +MINIMAPS_ENABLED + +## System command that invokes youtube-dl, used by Play Internet Sound. +## You can install youtube-dl with +## "pip install youtube-dl" if you have pip installed +## from https://github.com/rg3/youtube-dl/releases +## or your package manager +## The default value assumes youtube-dl is in your system PATH +# INVOKE_YOUTUBEDL youtube-dl + +## CLIENT VERSION CONTROL +## This allows you to configure the minimum required client version, as well as a warning version, and message for both. +## These trigger for any version below (non-inclusive) the given version, so 510 triggers on 509 or lower. +## These messages will be followed by one stating the clients current version and the required version for clarity. +## If CLIENT_WARN_POPUP is uncommented a popup window with the message will be displayed instead +#CLIENT_WARN_VERSION 511 +#CLIENT_WARN_POPUP +#CLIENT_WARN_MESSAGE Byond released 511 as the stable release. You can set the framerate your client runs at, which makes the game feel very different and cool. Shortly after its release we will end up using 511 client features and you will be forced to update. +CLIENT_ERROR_VERSION 511 +CLIENT_ERROR_MESSAGE Your version of byond is not supported. Please upgrade. +## The minimum build needed for joining the server, if using 512, a good minimum build would be 1421 as that disables the Middle Mouse Button exploit. +CLIENT_ERROR_BUILD 1421 + +## Assets can opt-in to caching their results into `tmp`. +## This is important, as preferences assets take upwards of 30 seconds (without sleeps) to collect. +## The cache is assumed to be cleared by TGS recompiling, which deletes `tmp`. +## This should be disabled (through `CACHE_ASSETS 0`) on development, +## but enabled on production (the default). +CACHE_ASSETS 0 + +## Set to 0 to disable admins being able to have snowflake OOC colors +ALLOW_ADMIN_OOCCOLOR + +## Uncomment to set the number of /world/Reboot()s before the DreamDaemon restarts itself. 0 means restart every round. Requires tgstation server tools. +#ROUNDS_UNTIL_HARD_RESTART 5 + +## Comment to disable sending a toast notification on the host server when initializations complete. +## Even if this is enabled, a notification will only be sent if there are no clients connected. +TOAST_NOTIFICATION_ON_INIT diff --git a/config/entries/hub.txt b/config/entries/hub.txt deleted file mode 100644 index 9d3b876c128..00000000000 --- a/config/entries/hub.txt +++ /dev/null @@ -1,12 +0,0 @@ -## Community short-name: This appears in parentheses after the server's active station name -# COMMUNITYSHORTNAME citadel - -## Community link: Requires a community short-name defined. Attaches a link to the community's shortname, and requires a short-name defined. -## You could alternatively just add raw HTML to the above, but this is more newbie-friendly -# COMMUNITYLINK http://crouton.net - -## TAGLINE - Hub tagline -TAGLINE
Roleplay focused 18+ server with extensive species choices.
- -## Use tagline strings: Uncomment to have the server's tagline be randomly selected from taglines.txt -# USETAGLINESTRINGS diff --git a/config/entries/lobby.txt b/config/entries/lobby.txt index ff7c9f013ef..194ab2f6618 100644 --- a/config/entries/lobby.txt +++ b/config/entries/lobby.txt @@ -19,3 +19,16 @@ LOBBY_GAMEMODE_VOTE_DELAY 60 ## Enforce flavor text to be set before joining # ENFORCE_FLAVOR_TEXT + +## Community short-name: This appears in parentheses after the server's active station name +# COMMUNITYSHORTNAME citadel + +## Community link: Requires a community short-name defined. Attaches a link to the community's shortname, and requires a short-name defined. +## You could alternatively just add raw HTML to the above, but this is more newbie-friendly +# COMMUNITYLINK http://crouton.net + +## TAGLINE - Hub tagline +TAGLINE
Roleplay focused 18+ server with extensive species choices.
+ +## Use tagline strings: Uncomment to have the server's tagline be randomly selected from taglines.txt +# USETAGLINESTRINGS diff --git a/config/entries/logging.txt b/config/entries/logging.txt new file mode 100644 index 00000000000..8161de9c076 --- /dev/null +++ b/config/entries/logging.txt @@ -0,0 +1,5 @@ +## Log all TGUI in /Topic, regardless of handling. +# EMERGENCY_TGUI_LOGGING + +## Log all suspicious logins to file. +LOG_SUSPICIOUS_LOGIN diff --git a/config/entries/playtime.txt b/config/entries/playtime.txt new file mode 100644 index 00000000000..ab83b5f4add --- /dev/null +++ b/config/entries/playtime.txt @@ -0,0 +1,7 @@ +### PLAYTIME TRACKING +## Enable playtime tracking +PLAYTIME_TRACKING +## Enable job playtime restrictions +# JOB_CHECK_PLAYTIME +## Enable job account age restrictions +# JOB_CHECK_ACCOUNT_AGE diff --git a/config/resources.txt b/config/entries/resources.txt similarity index 100% rename from config/resources.txt rename to config/entries/resources.txt diff --git a/config/entries/security.txt b/config/entries/security.txt new file mode 100644 index 00000000000..56039c6dcb4 --- /dev/null +++ b/config/entries/security.txt @@ -0,0 +1,54 @@ +### FAIL2TOPIC: +### Automated IP bans for world/Topic() spammers +## Enabled +FAIL2TOPIC_ENABLED +## Minimum wait time in deciseconds between valid requests +FAIL2TOPIC_RATE_LIMIT 10 +## Number of requests after breaching rate limit that triggers a ban +FAIL2TOPIC_MAX_FAILS 5 +## Firewall rule name used on physical server +FAIL2TOPIC_RULE_NAME _dd_fail2topic + +## HARD topic limit, even without fail2topic +TOPIC_MAX_SIZE 1048576 + +### FAIL2TOPIC WHITELISTING +## Just list IPs like so: +# TOPIC_RATE_LIMIT_WHITELIST 351.531.632.145 obviously not valid but you get the point +TOPIC_RATE_LIMIT_WHITELIST 127.0.0.1 + +### IPINTEL +## IPIntel system enabled? +# IPINTEL_ENABLED +## Email given to API for contact - required. +# IPINTEL_EMAIL ch@nge.me +## The threshold of where we consider someone to be a VPN +IPINTEL_RATING_BAD 1 +## The hours we cache a good ipintel rating for +IPINTEL_SAVE_GOOD 12 +## The hours we cache a bad ipintel rating for +IPINTEL_SAVE_BAD 1 +## Ipintel API domain +IPINTEL_DOMAIN check.getipintel.net + +### AGE VERIFICATION +## Automated Age Verification +# AGE_VERIFICATION +## Autoban if someone's under 18 +AGE_VERIFICATION_AUTOBAN + +### MISC SECURITY CHECKS +## Check for CID Randomizers +CHECK_CID_RANDOMIZER + +### PANIC BUNKER +## Enable full panic bunker +# PANIC_BUNKER +## Enable panic bunker to detected VPNs +# VPN_BUNKER +## Set to use playtime tracking instead of raw connected-before check in bunker. Value is in minutes of playtime. +# PANIC_BUNKER_PLAYTIME 120 +## Set to show players a message if they try to join during panic bunker. +PANIC_BUNKER_MESSAGE Sorry, but the server is currently not accepting connections from never seen before players. +## How long to keep someone in bunker bypass list once whitelisted by admins +PANIC_BUNKER_BYPASS 7 diff --git a/config/entries/urls.txt b/config/entries/urls.txt index 7e3945dd104..c578a2dd139 100644 --- a/config/entries/urls.txt +++ b/config/entries/urls.txt @@ -1,2 +1,5 @@ ## Github address GITHUBURL https://github.com/Citadel-Station-13/Citadel-Station-13-RP + +## Wiki Page Root +WIKI_PAGE_ROOT https://citadel-station.net/wikiRP/index.php?title= diff --git a/tgui/packages/tgui/interfaces/AgeVerifyMenu.tsx b/tgui/packages/tgui/interfaces/AgeVerifyMenu.tsx new file mode 100644 index 00000000000..f8e3aa8310d --- /dev/null +++ b/tgui/packages/tgui/interfaces/AgeVerifyMenu.tsx @@ -0,0 +1,129 @@ +import { useBackend, useLocalState } from "../backend"; +import { Button, NoticeBox, Section, Stack } from "../components"; +import { Window } from "../layouts"; + +const MONTH_DAYS_LOOKUP: number[] = [ + 31, + 28, + 31, + 30, + 31, + 30, + 31, + 31, + 30, + 31, + 30, + 31, +]; + +const daysInMonth = (month: number, year: number) => { + if (Number.isNaN(month) || Number.isNaN(year)) { + return 31; + } + let days = MONTH_DAYS_LOOKUP[month]; + if ((month === 2) && ((year % 4) === 0)) { + days = 29; + } + return days; +}; + +const validYears = () => { + let current = (new Date()).getFullYear(); + let ret: string[] = []; + for (let i = current; i >= 1950; i--) { + ret.push(i.toString()); + } + return ret; +}; +const VALID_YEARS = validYears(); + +const VALID_MONTHS = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +].map((n) => n.toString()); + +const validDays = (month: number, year: number) => { + let current = daysInMonth(month, year); + let ret: string[] = []; + for (let i = 1; i <= current; i++) { + ret.push(i.toString()); + } + return ret; +}; + +export const AgeVerifyMenu = (props, context) => { + let { data, act } = useBackend(context); + let [month, setMonth] = useLocalState(context, "month", null); + let [day, setDay] = useLocalState(context, "day", null); + let [year, setYear] = useLocalState(context, "year", null); + return ( + + +
+ + + + Please enter your date of birth to continue. + + + + + + { + validDays( + (month === null)? 1 : Number.parseInt(month, 10), + (year === null)? 1 : Number.parseInt(year, 10) + ).map((m) => ( +
+
+
+ ); +};