diff --git a/SQL/Aurora_SQL_Schema.sql b/SQL/Aurora_SQL_Schema.sql
index a43ba9f001b..d3963e89367 100644
--- a/SQL/Aurora_SQL_Schema.sql
+++ b/SQL/Aurora_SQL_Schema.sql
@@ -277,6 +277,8 @@ CREATE TABLE `ss13_player_preferences` (
`UI_style_alpha` int(11) NOT NULL,
`be_special` int(11) NOT NULL,
`asfx_togs` int(11) NOT NULL,
+ `lastmotd` text NOT NULL,
+ `lastmemo` text NOT NULL,
PRIMARY KEY (`ckey`),
CONSTRAINT `player_preferences_fk_ckey` FOREIGN KEY (`ckey`) REFERENCES `ss13_player` (`ckey`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
diff --git a/baystation12.dme b/baystation12.dme
index e4627f614b0..9871920c126 100644
--- a/baystation12.dme
+++ b/baystation12.dme
@@ -134,6 +134,7 @@
#include "code\datums\modules.dm"
#include "code\datums\organs.dm"
#include "code\datums\recipe.dm"
+#include "code\datums\server_greeting.dm"
#include "code\datums\sun.dm"
#include "code\datums\supplypacks.dm"
#include "code\datums\diseases\appendicitis.dm"
@@ -811,8 +812,8 @@
#include "code\modules\admin\admin.dm"
#include "code\modules\admin\admin_attack_log.dm"
#include "code\modules\admin\admin_investigate.dm"
-#include "code\modules\admin\admin_memo.dm"
#include "code\modules\admin\admin_ranks.dm"
+#include "code\modules\admin\admin_server_greeting.dm"
#include "code\modules\admin\admin_verbs.dm"
#include "code\modules\admin\banjob.dm"
#include "code\modules\admin\create_mob.dm"
@@ -896,6 +897,7 @@
#include "code\modules\client\preferences_ambience.dm"
#include "code\modules\client\preferences_factions.dm"
#include "code\modules\client\preferences_gear.dm"
+#include "code\modules\client\preferences_notification.dm"
#include "code\modules\client\preferences_save_migration.dm"
#include "code\modules\client\preferences_savefile.dm"
#include "code\modules\client\preferences_spawnpoints.dm"
diff --git a/code/datums/server_greeting.dm b/code/datums/server_greeting.dm
new file mode 100644
index 00000000000..4e5741486aa
--- /dev/null
+++ b/code/datums/server_greeting.dm
@@ -0,0 +1,209 @@
+/*
+ * Server greeting datum.
+ * Contains the following information:
+ * - hashes for the message of the day and staff memos
+ * - the current message of the day and staff memos
+ * - the fully parsed welcome screen HTML data
+ */
+
+#define MEMOFILE "data/greeting.sav"
+
+#define OUTDATED_MOTD 1
+#define OUTDATED_MEMO 2
+#define OUTDATED_NOTE 4
+
+/datum/server_greeting
+ // Hashes to figure out if we need to display the greeting message.
+ // These correspond to motd_hash and memo_hash on /datum/preferences for each client.
+ var/motd_hash = ""
+ var/memo_hash = ""
+
+ // The stored strings of general subcomponents.
+ var/motd = ""
+ var/memo_list[] = list()
+ var/memo = ""
+
+ var/raw_data_user = ""
+ var/raw_data_staff = ""
+ // The near-final string to be displayed.
+ // Only one placeholder remains: .
+ var/user_data = ""
+ var/staff_data = ""
+
+/datum/server_greeting/New()
+ ..()
+
+ load_from_file()
+
+ prepare_data()
+
+/*
+ * Populates variables from save file, and loads the raw HTML data.
+ * Needs to be called at least once for successful initialization.
+ */
+/datum/server_greeting/proc/load_from_file()
+ var/savefile/F = new(MEMOFILE)
+ if (F)
+ if (F["motd"])
+ F["motd"] >> motd
+
+ if (F["memo"])
+ F["memo"] >> memo_list
+
+ raw_data_staff = file2text('html/templates/welcome_screen.html')
+
+ // This is a lazy way, but it disables the user from being able to see the memo button.
+ var/staff_button = "
Staff Memos"
+ raw_data_user = replacetextEx(raw_data_staff, staff_button, "")
+
+/*
+ * Generates hashes, placeholders, and reparses var/memo.
+ * Then updates staff_data and user_data with the new contents.
+ * To be called after load_from_file or update_value.
+ */
+/datum/server_greeting/proc/prepare_data()
+ if (!motd)
+ motd = "No new announcements to showcase."
+ motd_hash = ""
+ else
+ motd_hash = md5(motd)
+
+ memo = ""
+
+ if (memo_list.len)
+ for (var/ckey in memo_list)
+ var/data = {"
+ [ckey] wrote on [memo_list[ckey]["date"]]:
+ [memo_list[ckey]["content"]]
+ "}
+
+ memo += data
+
+ memo_hash = md5(memo)
+ else
+ memo = "No memos have been posted."
+ memo_hash = ""
+
+ var/html_one = raw_data_staff
+ html_one = replacetextEx(html_one, "", motd)
+ html_one = replacetextEx(html_one, "", memo)
+ staff_data = html_one
+
+ var/html_two = raw_data_user
+ html_two = replacetextEx(html_two, "", motd)
+ user_data = html_two
+
+ return
+
+/*
+ * Helper to update the MoTD or memo contents.
+ * Args:
+ * - var/change string
+ * - var/new_value mixed
+ * Returns:
+ * - 1 upon success
+ * - 0 upon failure
+ */
+/datum/server_greeting/proc/update_value(var/change, var/new_value)
+ if (!change || !new_value)
+ return 0
+
+ switch (change)
+ if ("motd")
+ motd = new_value
+ motd_hash = md5(new_value)
+
+ if ("memo_write")
+ memo_list[new_value[1]] = list("date" = time2text(world.realtime, "DD-MMM-YYYY"), "content" = new_value[2])
+
+ if ("memo_delete")
+ if (memo_list[new_value])
+ memo_list -= new_value
+ else
+ return 0
+
+ else
+ return 0
+
+ var/savefile/F = new(MEMOFILE)
+ F["motd"] << motd
+ F["memo"] << memo_list
+
+ prepare_data()
+
+ return 1
+
+/*
+ * Helper proc to determine whether or not we need to show the greeting window to a user.
+ * Args:
+ * - var/user client
+ * Returns:
+ * - int
+ */
+/datum/server_greeting/proc/find_outdated_info(var/client/user)
+ if (!user || !user.prefs)
+ return 0
+
+ var/outdated_info = 0
+
+ if (motd_hash && user.prefs.motd_hash != motd_hash)
+ outdated_info |= OUTDATED_MOTD
+
+ if (user.holder && memo_hash && user.prefs.memo_hash != memo_hash)
+ outdated_info |= OUTDATED_MEMO
+
+ if (user.prefs.notifications.len)
+ outdated_info |= OUTDATED_NOTE
+
+ return outdated_info
+
+/*
+ * Composes the final message and displays it to the user.
+ * Also clears the user's notifications, should he have any.
+ */
+/datum/server_greeting/proc/display_to_client(var/client/user, var/outdated_info = 0)
+ if (!user)
+ return
+
+ var/notifications = "You do not have any notifications to show.
"
+ var/list/outdated_tabs = list()
+ var/save_prefs = 0
+
+ if (outdated_info & OUTDATED_NOTE)
+ outdated_tabs += "#note-tab"
+
+ notifications = ""
+ for (var/datum/client_notification/a in user.prefs.notifications)
+ notifications += a.get_html()
+
+ if (outdated_info & OUTDATED_MEMO)
+ outdated_tabs += "#memo-tab"
+ user.prefs.memo_hash = memo_hash
+ save_prefs = 1
+
+ if (outdated_info & OUTDATED_MOTD)
+ outdated_tabs += "#motd-tab"
+ user.prefs.motd_hash = motd_hash
+ save_prefs = 1
+
+ var/data = user_data
+
+ if (user.holder)
+ data = staff_data
+
+ data = replacetextEx(data, "", notifications)
+
+ if (outdated_tabs.len)
+ var/tab_string = json_encode(outdated_tabs)
+ data = replacetextEx(data, "var updated_tabs = \[\]", "var updated_tabs = [tab_string]")
+
+ user << browse(data, "window=welcome_screen;size=640x500")
+
+ if (save_prefs)
+ user.prefs.handle_preferences_save(user)
+
+#undef OUTDATED_NOTE
+#undef OUTDATED_MEMO
+#undef OUTDATED_MOTD
+
+#undef MEMOFILE
diff --git a/code/global.dm b/code/global.dm
index 9756bb67f62..2dbd6d65b9e 100644
--- a/code/global.dm
+++ b/code/global.dm
@@ -179,7 +179,7 @@ var/datum/moduletypes/mods = new()
var/wavesecret = 0
var/gravity_is_on = 1
-var/join_motd = null
+var/datum/server_greeting/server_greeting = null
var/forceblob = 0
var/datum/nanomanager/nanomanager = new() // NanoManager, the manager for Nano UIs.
diff --git a/code/modules/admin/DB ban/ban_mirroring.dm b/code/modules/admin/DB ban/ban_mirroring.dm
index 0ecc78a62dc..56e9a26efc2 100644
--- a/code/modules/admin/DB ban/ban_mirroring.dm
+++ b/code/modules/admin/DB ban/ban_mirroring.dm
@@ -113,8 +113,6 @@
var/DBQuery/query = dbcon.NewQuery("SELECT ban_mirror_id, player_ckey, ban_mirror_ip, ban_mirror_computerid, date(ban_mirror_datetime) as datetime FROM ss13_ban_mirrors WHERE ban_id = :ban_id")
query.Execute(list(":ban_id" = ban_id))
- testing("Ban ID: [ban_id]")
-
var/mirrors[] = list()
while (query.NextRow())
var/items[] = list()
diff --git a/code/modules/admin/admin_memo.dm b/code/modules/admin/admin_memo.dm
deleted file mode 100644
index 4bcaf10d9c7..00000000000
--- a/code/modules/admin/admin_memo.dm
+++ /dev/null
@@ -1,54 +0,0 @@
-#define MEMOFILE "data/memo.sav" //where the memos are saved
-#define ENABLE_MEMOS 1 //using a define because screw making a config variable for it. This is more efficient and purty.
-
-//switch verb so we don't spam up the verb lists with like, 3 verbs for this feature.
-/client/proc/admin_memo(task in list("write","show","delete"))
- set name = "Memo"
- set category = "Server"
- if(!ENABLE_MEMOS) return
- if(!check_rights(0)) return
- switch(task)
- if("write") admin_memo_write()
- if("show") admin_memo_show()
- if("delete") admin_memo_delete()
-
-//write a message
-/client/proc/admin_memo_write()
- var/savefile/F = new(MEMOFILE)
- if(F)
- var/memo = sanitize(input(src,"Type your memo\n(Leaving it blank will delete your current memo):","Write Memo",null) as null|message, extra = 0)
- switch(memo)
- if(null)
- return
- if("")
- F.dir.Remove(ckey)
- src << "Memo removed"
- return
- if( findtext(memo,"
+
+
+
+
+
+