diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index c0b2c17b0a3..14c8adcc6e7 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -7,7 +7,9 @@
# Actual Code
/code/controllers/subsystem/instancing.dm @AffectedArc07
/code/controllers/subsystem/mapping.dm @AffectedArc07
+/code/controllers/subsystem/redis.dm @AffectedArc07
/code/controllers/subsystem/ticker.dm @AffectedArc07
+/code/modules/redis/ @AffectedArc07
# CI + Tooling
/.github/workflows/ @AffectedArc07
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5416ece4b33..44ed933ab24 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -84,6 +84,10 @@ jobs:
sudo apt update || true
sudo apt install libssl1.1:i386
ldd librust_g.so
+ - name: Start Redis
+ uses: supercharge/redis-github-action@1.4.0
+ with:
+ redis-version: 6
- name: Compile & Run Unit Tests
run: |
tools/ci/install_byond.sh
diff --git a/code/__DEFINES/_versions.dm b/code/__DEFINES/_versions.dm
index 83a18de6f73..5670b470a8d 100644
--- a/code/__DEFINES/_versions.dm
+++ b/code/__DEFINES/_versions.dm
@@ -1,2 +1,2 @@
/// Version of RUST-G that this codebase wants
-#define RUST_G_VERSION "0.5.0-P"
+#define RUST_G_VERSION "0.6.0-P"
diff --git a/code/__DEFINES/rust_g.dm b/code/__DEFINES/rust_g.dm
index 9c81400ae2a..e134f8ef99e 100644
--- a/code/__DEFINES/rust_g.dm
+++ b/code/__DEFINES/rust_g.dm
@@ -41,6 +41,49 @@
/// Gets the version of rust_g
/proc/rustg_get_version() return call(RUST_G, "get_version")()
+
+/**
+ * Sets up the Aho-Corasick automaton with its default options.
+ *
+ * The search patterns list and the replacements must be of the same length when replace is run, but an empty replacements list is allowed if replacements are supplied with the replace call
+ * Arguments:
+ * * key - The key for the automaton, to be used with subsequent rustg_acreplace/rustg_acreplace_with_replacements calls
+ * * patterns - A non-associative list of strings to search for
+ * * replacements - Default replacements for this automaton, used with rustg_acreplace
+ */
+#define rustg_setup_acreplace(key, patterns, replacements) call(RUST_G, "setup_acreplace")(key, json_encode(patterns), json_encode(replacements))
+
+/**
+ * Sets up the Aho-Corasick automaton using supplied options.
+ *
+ * The search patterns list and the replacements must be of the same length when replace is run, but an empty replacements list is allowed if replacements are supplied with the replace call
+ * Arguments:
+ * * key - The key for the automaton, to be used with subsequent rustg_acreplace/rustg_acreplace_with_replacements calls
+ * * options - An associative list like list("anchored" = 0, "ascii_case_insensitive" = 0, "match_kind" = "Standard"). The values shown on the example are the defaults, and default values may be omitted. See the identically named methods at https://docs.rs/aho-corasick/latest/aho_corasick/struct.AhoCorasickBuilder.html to see what the options do.
+ * * patterns - A non-associative list of strings to search for
+ * * replacements - Default replacements for this automaton, used with rustg_acreplace
+ */
+#define rustg_setup_acreplace_with_options(key, options, patterns, replacements) call(RUST_G, "setup_acreplace")(key, json_encode(options), json_encode(patterns), json_encode(replacements))
+
+/**
+ * Run the specified replacement engine with the provided haystack text to replace, returning replaced text.
+ *
+ * Arguments:
+ * * key - The key for the automaton
+ * * text - Text to run replacements on
+ */
+#define rustg_acreplace(key, text) call(RUST_G, "acreplace")(key, text)
+
+/**
+ * Run the specified replacement engine with the provided haystack text to replace, returning replaced text.
+ *
+ * Arguments:
+ * * key - The key for the automaton
+ * * text - Text to run replacements on
+ * * replacements - Replacements for this call. Must be the same length as the set-up patterns
+ */
+#define rustg_acreplace_with_replacements(key, text, replacements) call(RUST_G, "acreplace_with_replacements")(key, text, json_encode(replacements))
+
// Cellular Noise Operations //
/**
@@ -124,6 +167,14 @@
// Noise Operations //
#define rustg_noise_get_at_coordinates(seed, x, y) call(RUST_G, "noise_get_at_coordinates")(seed, x, y)
+#define RUSTG_REDIS_ERROR_CHANNEL "RUSTG_REDIS_ERROR_CHANNEL"
+
+#define rustg_redis_connect(addr) call(RUST_G, "redis_connect")(addr)
+/proc/rustg_redis_disconnect() return call(RUST_G, "redis_disconnect")()
+#define rustg_redis_subscribe(channel) call(RUST_G, "redis_subscribe")(channel)
+/proc/rustg_redis_get_messages() return call(RUST_G, "redis_get_messages")()
+#define rustg_redis_publish(channel, message) call(RUST_G, "redis_publish")(channel, message)
+
// SQL Opeartions //
#define rustg_sql_connect_pool(options) call(RUST_G, "sql_connect_pool")(options)
#define rustg_sql_query_async(handle, query, params) call(RUST_G, "sql_query_async")(handle, query, params)
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index 3dcbfa017aa..d98b9bd210f 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -50,8 +50,9 @@
#define INIT_ORDER_PROFILER 101
#define INIT_ORDER_QUEUE 100 // Load this quickly so people cant queue skip
#define INIT_ORDER_TITLE 99 // Load this quickly so people dont see a blank lobby screen
-#define INIT_ORDER_GARBAGE 21
-#define INIT_ORDER_DBCORE 20
+#define INIT_ORDER_GARBAGE 22
+#define INIT_ORDER_DBCORE 21
+#define INIT_ORDER_REDIS 20 // Make sure we dont miss any events
#define INIT_ORDER_BLACKBOX 19
#define INIT_ORDER_CLEANUP 18
#define INIT_ORDER_INPUT 17
diff --git a/code/controllers/configuration/configuration_core.dm b/code/controllers/configuration/configuration_core.dm
index c96e96016e8..717edae8a54 100644
--- a/code/controllers/configuration/configuration_core.dm
+++ b/code/controllers/configuration/configuration_core.dm
@@ -38,6 +38,8 @@ GLOBAL_DATUM_INIT(configuration, /datum/server_configuration, new())
var/datum/configuration_section/movement_configuration/movement
/// Holder for the overflow configuration datum
var/datum/configuration_section/overflow_configuration/overflow
+ /// Holder for the redis configuration datum
+ var/datum/configuration_section/redis_configuration/redis
/// Holder for the ruins configuration datum
var/datum/configuration_section/ruin_configuration/ruins
/// Holder for the system configuration datum
@@ -88,6 +90,7 @@ GLOBAL_DATUM_INIT(configuration, /datum/server_configuration, new())
metrics = new()
movement = new()
overflow = new()
+ redis = new()
ruins = new()
system = new()
url = new()
@@ -125,6 +128,7 @@ GLOBAL_DATUM_INIT(configuration, /datum/server_configuration, new())
safe_load(metrics, "metrics_configuration")
safe_load(movement, "movement_configuration")
safe_load(overflow, "overflow_configuration")
+ safe_load(redis, "redis_configuration")
safe_load(ruins, "ruin_configuration")
safe_load(system, "system_configuration")
safe_load(url, "url_configuration")
diff --git a/code/controllers/configuration/sections/redis_configuration.dm b/code/controllers/configuration/sections/redis_configuration.dm
new file mode 100644
index 00000000000..347cae9282f
--- /dev/null
+++ b/code/controllers/configuration/sections/redis_configuration.dm
@@ -0,0 +1,20 @@
+/// Config holder for all redis related things
+/datum/configuration_section/redis_configuration
+ protection_state = PROTECTION_PRIVATE // NO! BAD!
+ /// Redis enabled or not
+ var/enabled = FALSE
+ /// Redis connection string. Includes passphrase if needed.
+ var/connstring = "redis://127.0.0.1/"
+
+/datum/configuration_section/redis_configuration/load_data(list/data)
+ // UNIT TESTS ARE DEFINED - USE CUSTOM CI VALUES
+ #ifdef UNIT_TESTS
+
+ enabled = TRUE
+
+ #else
+ // Load the normal config. Were not in CI mode
+ // Use the load wrappers here. That way the default isnt made 'null' if you comment out the config line
+ CONFIG_LOAD_BOOL(enabled, data["redis_enabled"])
+ CONFIG_LOAD_STR(connstring, data["redis_connstring"])
+ #endif
diff --git a/code/controllers/subsystem/redis.dm b/code/controllers/subsystem/redis.dm
new file mode 100644
index 00000000000..df8ce561595
--- /dev/null
+++ b/code/controllers/subsystem/redis.dm
@@ -0,0 +1,107 @@
+SUBSYSTEM_DEF(redis)
+ name = "Redis"
+ init_order = INIT_ORDER_REDIS
+ runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY // ALL THE THINGS
+ wait = 1
+ flags = SS_TICKER // Every tick
+ /// Are we connected
+ var/connected = FALSE
+ /// Amount of subscribed channels on the redis server
+ var/list/subbed_channels = list()
+ /// Message queue (If messages are sent before the SS has init'd)
+ var/list/datum/redis_message/queue = list()
+ offline_implications = "The server will no longer be able to send or receive redis messages. Shuttle call recommended (Potential server crash inbound)."
+
+// SS meta procs
+/datum/controller/subsystem/redis/stat_entry()
+ ..("S:[length(subbed_channels)] | Q:[length(queue)] | C:[connected ? "Y" : "N"]")
+
+/datum/controller/subsystem/redis/Initialize()
+ // Connect to cappuccino
+ connect()
+
+ if(connected)
+ // Loop efficiency doesnt matter here. It runs once and likely wont have any events in
+ for(var/datum/redis_message/RM in queue)
+ publish(RM.channel, RM.message)
+
+ // Setup all callbacks
+ for(var/cb in subtypesof(/datum/redis_callback))
+ var/datum/redis_callback/RCB = new cb()
+ if(isnull(RCB.channel))
+ stack_trace("[RCB.type] has no channel set!")
+ continue
+
+ if(RCB.channel in subbed_channels)
+ stack_trace("Attempted to subscribe to the channel '[RCB.channel]' from [RCB.type] twice!")
+
+ rustg_redis_subscribe(RCB.channel)
+ subbed_channels[RCB.channel] = RCB
+
+ var/amount_registered = length(subbed_channels)
+ log_startup_progress("Registered [amount_registered] callback[amount_registered == 1 ? "" : "s"].")
+
+ return ..()
+
+/datum/controller/subsystem/redis/fire()
+ check_messages()
+
+
+// Redis integration stuff
+/datum/controller/subsystem/redis/proc/connect()
+ if(GLOB.configuration.redis.enabled)
+ var/conn_failed = rustg_redis_connect(GLOB.configuration.redis.connstring)
+ if(conn_failed)
+ log_startup_progress("Failed to connect to redis. Please inform the server host.")
+ SEND_TEXT(world.log, "Redis connection failure: [conn_failed]")
+ return
+
+ connected = TRUE
+
+/datum/controller/subsystem/redis/proc/disconnect()
+ rustg_redis_disconnect()
+ connected = FALSE
+
+/datum/controller/subsystem/redis/proc/check_messages()
+ var/raw_data = rustg_redis_get_messages()
+ var/list/usable_data
+
+ try // Did you know byond had try catch?
+ usable_data = json_decode(raw_data)
+ catch
+ message_admins("Failed to deserialise a redis message | Please inform the server host.")
+ log_debug("Redis raw data: [raw_data]")
+ return
+
+ for(var/channel in usable_data)
+ if(channel == RUSTG_REDIS_ERROR_CHANNEL)
+ message_admins("Redis error: [usable_data[channel]] | Please inform the server host.") // uh oh
+ continue
+ // Check its an actual channel
+ if(!(channel in subbed_channels))
+ stack_trace("Recieved a message on the channel '[channel]' when we arent subscribed to it. What the heck?")
+ continue
+
+ var/datum/redis_callback/RCB = subbed_channels[channel]
+ for(var/message in usable_data[channel])
+ RCB.on_message(message)
+
+/datum/controller/subsystem/redis/proc/publish(channel, message)
+ // If we arent alive, queue
+ if(!connected)
+ var/datum/redis_message/RM = new()
+ RM.channel = channel
+ RM.message = message
+ queue += RM
+ return
+
+ // If we are alive, publish straight away
+ rustg_redis_publish(channel, message)
+
+
+// Misc protection stuff
+/datum/controller/subsystem/redis/CanProcCall(procname)
+ return FALSE
+
+/datum/controller/subsystem/redis/vv_edit_var(var_name, var_value)
+ return FALSE // dont even try
diff --git a/code/game/world.dm b/code/game/world.dm
index 28a270b2543..11774cf88f2 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -285,4 +285,5 @@ GLOBAL_LIST_EMPTY(world_topic_handlers)
/world/Del()
rustg_close_async_http_client() // Close the HTTP client. If you dont do this, youll get phantom threads which can crash DD from memory access violations
disable_auxtools_debugger() // Disables the debugger if running. See above comment
+ rustg_redis_disconnect() // Disconnects the redis connection. See above.
..()
diff --git a/code/modules/admin/verbs/adminsay.dm b/code/modules/admin/verbs/adminsay.dm
index 4005d9b7435..ff3bcaf843d 100644
--- a/code/modules/admin/verbs/adminsay.dm
+++ b/code/modules/admin/verbs/adminsay.dm
@@ -12,6 +12,14 @@
log_adminsay(msg, src)
if(check_rights(R_ADMIN,0))
+ // Do this up here before it gets sent to everyone & emoji'd
+ if(SSredis.connected)
+ var/list/data = list()
+ data["author"] = usr.ckey
+ data["source"] = GLOB.configuration.system.instance_id
+ data["message"] = msg
+ SSredis.publish("byond.asay.out", json_encode(data))
+
for(var/client/C in GLOB.admins)
if(R_ADMIN & C.holder.rights)
// Lets see if this admin was pinged in the asay message
@@ -48,6 +56,14 @@
if(!msg)
return
+ // Do this up here before it gets sent to everyone & emoji'd
+ if(SSredis.connected)
+ var/list/data = list()
+ data["author"] = usr.ckey
+ data["source"] = GLOB.configuration.system.instance_id
+ data["message"] = msg
+ SSredis.publish("byond.msay.out", json_encode(data))
+
for(var/client/C in GLOB.admins)
if(check_rights(R_ADMIN|R_MOD|R_MENTOR, 0, C.mob))
var/display_name = key
diff --git a/code/modules/redis/callbacks/asay_callback.dm b/code/modules/redis/callbacks/asay_callback.dm
new file mode 100644
index 00000000000..653034fb7a9
--- /dev/null
+++ b/code/modules/redis/callbacks/asay_callback.dm
@@ -0,0 +1,9 @@
+// Relays messages to asay
+/datum/redis_callback/asay_in
+ channel = "byond.asay.in"
+
+/datum/redis_callback/asay_in/on_message(message)
+ var/list/data = json_decode(message)
+ for(var/client/C in GLOB.admins)
+ if(R_ADMIN & C.holder.rights)
+ to_chat(C, "ADMIN: [data["author"]]@[data["source"]]: [html_encode(data["message"])]")
diff --git a/code/modules/redis/callbacks/msay_callback.dm b/code/modules/redis/callbacks/msay_callback.dm
new file mode 100644
index 00000000000..cf6309d2234
--- /dev/null
+++ b/code/modules/redis/callbacks/msay_callback.dm
@@ -0,0 +1,10 @@
+// Relays messages to msay
+/datum/redis_callback/msay_in
+ channel = "byond.msay.in"
+
+/datum/redis_callback/msay_in/on_message(message)
+ var/list/data = json_decode(message)
+ for(var/client/C in GLOB.admins)
+ if(check_rights(R_ADMIN|R_MOD|R_MENTOR, FALSE, C.mob))
+ to_chat(C, "MENTOR: [data["author"]]@[data["source"]]: [html_encode(data["message"])]")
+
diff --git a/code/modules/redis/redis_callback.dm b/code/modules/redis/redis_callback.dm
new file mode 100644
index 00000000000..8a79c5f923b
--- /dev/null
+++ b/code/modules/redis/redis_callback.dm
@@ -0,0 +1,31 @@
+/**
+ * # Redis callback
+ *
+ * Callback datum for subscribed redis channel handling
+ *
+ * This datum is used for easily assigning callbacks for SSredis to use
+ * when a message is receievd on a channel. Define a channel on the `channel`
+ * var and SSredis will automatically register subtypes of [/datum/redis_callback]
+ */
+/datum/redis_callback
+ /// Channel for this callback to listen on
+ var/channel = null
+
+/**
+ * Message handler callback
+ *
+ * This callback is ran when a message is recieved on the assigned channel.
+ * Make sure you override it on subtypes or it wont work.
+ *
+ * Arguments:
+ * * message - The message received on the redis channel
+ */
+/datum/redis_callback/proc/on_message(message)
+ CRASH("on_message() not overriden for [type]!")
+
+// Misc protections
+/datum/redis_callback/vv_edit_var(var_name, var_value)
+ return FALSE // no
+
+/datum/redis_callback/CanProcCall(procname)
+ return FALSE // no
diff --git a/code/modules/redis/redis_message.dm b/code/modules/redis/redis_message.dm
new file mode 100644
index 00000000000..afdda7f6131
--- /dev/null
+++ b/code/modules/redis/redis_message.dm
@@ -0,0 +1,14 @@
+/**
+ * # Redis message
+ *
+ * Holder datum for redis messages
+ *
+ * This datum is used for caching messages that SSredis tries to
+ * publish before it has connected. It is not used for any subscribed
+ * channel handling.
+ */
+/datum/redis_message
+ /// Destination channel for this message
+ var/channel = null
+ /// Message for that channel
+ var/message = null
diff --git a/config/example/config.toml b/config/example/config.toml
index aa869d91c10..6828477e148 100644
--- a/config/example/config.toml
+++ b/config/example/config.toml
@@ -592,6 +592,19 @@ overflow_whitelist = ["keyhere", "anotherhere"]
################################################################
+[redis_configuration]
+# This section contains all the settings for using redis.
+# This is heavily tied into the Paradise production architecture and you probably dont need it.
+
+# Enable/disable redis
+redis_enabled = false
+# Redis connection string. Include your passphrase if needed
+redis_connstring = "redis://127.0.0.1/"
+
+
+################################################################
+
+
[ruin_configuration]
# This section contains configuration for all space ruins and lava ruins
diff --git a/librust_g.so b/librust_g.so
index 20e5d057417..d2d6fe1a059 100644
Binary files a/librust_g.so and b/librust_g.so differ
diff --git a/paradise.dme b/paradise.dme
index 35ba942fe3c..cbf92eaf46e 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -213,6 +213,7 @@
#include "code\controllers\configuration\sections\metrics_configuration.dm"
#include "code\controllers\configuration\sections\movement_configuration.dm"
#include "code\controllers\configuration\sections\overflow_configuration.dm"
+#include "code\controllers\configuration\sections\redis_configuration.dm"
#include "code\controllers\configuration\sections\ruin_configuration.dm"
#include "code\controllers\configuration\sections\system_configuration.dm"
#include "code\controllers\configuration\sections\url_configuration.dm"
@@ -258,6 +259,7 @@
#include "code\controllers\subsystem\profiler.dm"
#include "code\controllers\subsystem\radiation.dm"
#include "code\controllers\subsystem\radio.dm"
+#include "code\controllers\subsystem\redis.dm"
#include "code\controllers\subsystem\runechat.dm"
#include "code\controllers\subsystem\server_queue.dm"
#include "code\controllers\subsystem\shuttles.dm"
@@ -2369,6 +2371,10 @@
#include "code\modules\recycling\disposal-construction.dm"
#include "code\modules\recycling\disposal.dm"
#include "code\modules\recycling\sortingmachinery.dm"
+#include "code\modules\redis\redis_callback.dm"
+#include "code\modules\redis\redis_message.dm"
+#include "code\modules\redis\callbacks\asay_callback.dm"
+#include "code\modules\redis\callbacks\msay_callback.dm"
#include "code\modules\research\circuitprinter.dm"
#include "code\modules\research\designs.dm"
#include "code\modules\research\destructive_analyzer.dm"
diff --git a/rust_g.dll b/rust_g.dll
index e3c5d1831c3..1f146b50574 100644
Binary files a/rust_g.dll and b/rust_g.dll differ