diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml
index 11a821405e..e271a1d338 100644
--- a/.github/workflows/ci-pipeline.yml
+++ b/.github/workflows/ci-pipeline.yml
@@ -514,20 +514,21 @@ jobs:
- name: Run Live Tests # Logging here is weird because printing massive amounts of text on Windows runners is SLOW AS SHIT!!!
id: live-tests
+ shell: bash
run: |
cd tests/Tgstation.Server.Tests
- Start-Sleep -Seconds 10
- $ErrorActionPreference="SilentlyContinue"
- $test_output = dotnet test -c ${{ matrix.configuration }} --no-build --filter TestCategory=RequiresDatabase --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" --collect:"XPlat Code Coverage" --settings ../../build/ci.runsettings --results-directory ../../TestResults
- $succeeded = $?
- $ErrorActionPreference="Stop"
+ sleep 10
+ set +e
+ test_output=$(dotnet test -c ${{ matrix.configuration }} --no-build --filter TestCategory=RequiresDatabase --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" --collect:"XPlat Code Coverage" --settings ../../build/ci.runsettings --results-directory ../../TestResults)
+ succeeded=$?
+ set -e
cd ../..
- $test_output | Out-File -FilePath ./test_output.txt
- if (-Not $succeeded) {
- echo "succeeded=NO" >> $env:GITHUB_OUTPUT
- } else {
- echo "succeeded=YES" >> $env:GITHUB_OUTPUT
- }
+ echo $test_output > ./test_output.txt
+ if [[ $retVal -ne 0 ]]; then
+ echo "succeeded=NO" >> $GITHUB_OUTPUT
+ else
+ echo "succeeded=YES" >> $GITHUB_OUTPUT
+ fi
- name: Store Live Tests Output
uses: actions/upload-artifact@v3
diff --git a/docs/Features.dox b/docs/Features.dox
index 725b26d947..64eca5a3f7 100644
--- a/docs/Features.dox
+++ b/docs/Features.dox
@@ -87,4 +87,5 @@ tgstation-server is a BYOND server managment suite. It includes all the followin
- Functions with all 3 DreamDaemon security levels
- Provides notifications of TGS side events
- Allows specifying the .dmb's minimum required security level
+ - Broadcast messages to clients
*/
diff --git a/src/DMAPI/tgs/v5/_defines.dm b/src/DMAPI/tgs/v5/_defines.dm
index b4d3cc4108..1c7d67d20c 100644
--- a/src/DMAPI/tgs/v5/_defines.dm
+++ b/src/DMAPI/tgs/v5/_defines.dm
@@ -80,6 +80,7 @@
#define DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH 8
#define DMAPI5_TOPIC_COMMAND_SEND_CHUNK 9
#define DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK 10
+#define DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST 11
#define DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE "commandType"
#define DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND "chatCommand"
@@ -89,6 +90,7 @@
#define DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME "newInstanceName"
#define DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE "chatUpdate"
#define DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION "newServerVersion"
+#define DMAPI5_TOPIC_PARAMETER_BROADCAST_MESSAGE "broadcastMessage"
#define DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE "commandResponse"
#define DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE "commandResponseMessage"
diff --git a/src/DMAPI/tgs/v5/topic.dm b/src/DMAPI/tgs/v5/topic.dm
index 40ab80e465..05e6c4e1b2 100644
--- a/src/DMAPI/tgs/v5/topic.dm
+++ b/src/DMAPI/tgs/v5/topic.dm
@@ -94,7 +94,7 @@
if(DMAPI5_TOPIC_COMMAND_CHANGE_PORT)
var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT]
if (!isnum(new_port) || !(new_port > 0))
- return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]")
+ return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]")
if(event_handler != null)
event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port)
@@ -141,7 +141,7 @@
if(DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE)
var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT]
if (!isnum(new_port) || !(new_port > 0))
- return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]")
+ return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]")
server_port = new_port
return TopicResponse()
@@ -157,7 +157,7 @@
var/error_message = null
if (new_port != null)
if (!isnum(new_port) || !(new_port > 0))
- error_message = "Invalid [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]"
+ error_message = "Invalid [DMAPI5_TOPIC_PARAMETER_NEW_PORT]"
else
server_port = new_port
@@ -165,7 +165,7 @@
if (!istext(new_version_string))
if(error_message != null)
error_message += ", "
- error_message += "Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION]]"
+ error_message += "Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION]"
else
var/datum/tgs_version/new_version = new(new_version_string)
if (event_handler)
@@ -268,4 +268,16 @@
return chunk_to_send
+ if(DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST)
+ var/message = topic_parameters[DMAPI5_TOPIC_PARAMETER_BROADCAST_MESSAGE]
+ if (!istext(message))
+ return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_BROADCAST_MESSAGE]")
+
+ TGS_WORLD_ANNOUNCE(message)
+ return TopicResponse()
+
return TopicResponse("Unknown command: [command]")
+
+/datum/tgs_api/v5/proc/WorldBroadcast(message)
+ set waitfor = FALSE
+ TGS_WORLD_ANNOUNCE(message)
diff --git a/src/DMAPI/tgs/v5/undefs.dm b/src/DMAPI/tgs/v5/undefs.dm
index fbf7b23d59..d531d4b7b9 100644
--- a/src/DMAPI/tgs/v5/undefs.dm
+++ b/src/DMAPI/tgs/v5/undefs.dm
@@ -78,6 +78,9 @@
#undef DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE
#undef DMAPI5_TOPIC_COMMAND_HEALTHCHECK
#undef DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH
+#undef DMAPI5_TOPIC_COMMAND_SEND_CHUNK
+#undef DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK
+#undef DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST
#undef DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE
#undef DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND
@@ -87,6 +90,7 @@
#undef DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME
#undef DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE
#undef DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION
+#undef DMAPI5_TOPIC_PARAMETER_BROADCAST_MESSAGE
#undef DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE
#undef DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE
diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs
index d002d80e5c..52de54d77e 100644
--- a/src/Tgstation.Server.Api/Models/ErrorCode.cs
+++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs
@@ -629,6 +629,12 @@ namespace Tgstation.Server.Api.Models
[Description("The deployment took longer than the configured timeout!")]
DeploymentTimeout,
+ ///
+ /// Sending a broadcast message failed.
+ ///
+ [Description("Could not send broadcast to the DMAPI. This can happen either due to there being an insufficient DMAPI version, a communication failure, or the server being offline.")]
+ BroadcastFailure,
+
///
/// Could not compile OpenDream due to a missing dotnet executable.
///
diff --git a/src/Tgstation.Server.Api/Models/Request/DreamDaemonRequest.cs b/src/Tgstation.Server.Api/Models/Request/DreamDaemonRequest.cs
index 0398d69935..0e4c51e993 100644
--- a/src/Tgstation.Server.Api/Models/Request/DreamDaemonRequest.cs
+++ b/src/Tgstation.Server.Api/Models/Request/DreamDaemonRequest.cs
@@ -7,5 +7,9 @@ namespace Tgstation.Server.Api.Models.Request
///
public sealed class DreamDaemonRequest : DreamDaemonApiBase
{
+ ///
+ /// A to send to the running server's DMAPI for broadcasting. How this is displayed is up to how the DMAPI is integrated in the codebase. Requires interop version >=5.7.0.
+ ///
+ public string? BroadcastMessage { get; set; }
}
}
diff --git a/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs b/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs
index b13002bf9c..378783bf1e 100644
--- a/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs
+++ b/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs
@@ -112,5 +112,10 @@ namespace Tgstation.Server.Api.Rights
/// User can change .
///
SetMapThreads = 1 << 19,
+
+ ///
+ /// User can use .
+ ///
+ BroadcastMessage = 1 << 20,
}
}
diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs
index 29be2c9a10..286c605d07 100644
--- a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs
+++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs
@@ -62,5 +62,10 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// Receive additional data for a previous response.
///
ReceiveChunk,
+
+ ///
+ /// Sending a broadcast message.
+ ///
+ Broadcast,
}
}
diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs
index 3da7e1c34c..fff5453b0d 100644
--- a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs
+++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs
@@ -42,6 +42,11 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
///
public string NewInstanceName { get; }
+ ///
+ /// The message to broadcast for requests.
+ ///
+ public string BroadcastMessage { get; }
+
///
/// The for requests.
///
@@ -68,6 +73,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
or TopicCommandType.ChangeRebootState
or TopicCommandType.InstanceRenamed
or TopicCommandType.ChatChannelsUpdate
+ or TopicCommandType.Broadcast
or TopicCommandType.ServerRestarted => true,
TopicCommandType.ChatCommand
or TopicCommandType.HealthCheck
@@ -76,6 +82,26 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
_ => throw new InvalidOperationException($"Invalid value for {nameof(CommandType)}: {CommandType}"),
};
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of .
+ /// The created .
+ public static TopicParameters CreateInstanceRenamedTopicParameters(string newInstanceName)
+ => new (
+ newInstanceName ?? throw new ArgumentNullException(nameof(newInstanceName)),
+ TopicCommandType.InstanceRenamed);
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of .
+ /// The created .
+ public static TopicParameters CreateBroadcastParameters(string broadcastMessage)
+ => new (
+ broadcastMessage ?? throw new ArgumentNullException(nameof(broadcastMessage)),
+ TopicCommandType.Broadcast);
+
///
/// Initializes a new instance of the class.
///
@@ -116,16 +142,6 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
NewRebootState = newRebootState;
}
- ///
- /// Initializes a new instance of the class.
- ///
- /// The value of .
- public TopicParameters(string newInstanceName)
- : this(TopicCommandType.InstanceRenamed)
- {
- NewInstanceName = newInstanceName ?? throw new ArgumentNullException(nameof(newInstanceName));
- }
-
///
/// Initializes a new instance of the class.
///
@@ -175,5 +191,28 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
{
CommandType = commandType;
}
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The parameter for the property designated by .
+ /// The value of .
+ TopicParameters(string stringCommand, TopicCommandType stringCommandType)
+ : this(stringCommandType)
+ {
+#pragma warning disable IDE0010 // Add missing cases
+ switch (stringCommandType)
+ {
+ case TopicCommandType.InstanceRenamed:
+ NewInstanceName = stringCommand;
+ break;
+ case TopicCommandType.Broadcast:
+ BroadcastMessage = stringCommand;
+ break;
+ default:
+ throw new InvalidOperationException($"Invalid string TopicCommandType: {stringCommandType}");
+ }
+#pragma warning restore IDE0010 // Add missing cases
+ }
}
}
diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs
index 2f046830d0..73c9d82341 100644
--- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs
+++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs
@@ -534,7 +534,9 @@ namespace Tgstation.Server.Host.Components.Session
public async ValueTask InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
{
ReattachInformation.RuntimeInformation.InstanceName = newInstanceName;
- await SendCommand(new TopicParameters(newInstanceName), cancellationToken);
+ await SendCommand(
+ TopicParameters.CreateInstanceRenamedTopicParameters(newInstanceName),
+ cancellationToken);
}
///
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs
index 360d3fcfc3..2d8d0f5d24 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs
@@ -88,5 +88,13 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// The for the operation.
/// A representing the running operation.
ValueTask CreateDump(CancellationToken cancellationToken);
+
+ ///
+ /// Send a broadcast to the DMAPI.
+ ///
+ /// The message to broadcast.
+ /// The for the operation.
+ /// A resulting in if the broadcast succeeded., otherwise.
+ ValueTask Broadcast(string message, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
index 97fad794f3..6048e66f61 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
@@ -457,6 +457,43 @@ namespace Tgstation.Server.Host.Components.Watchdog
await session.CreateDump(dumpFileName, cancellationToken);
}
+ ///
+ public async ValueTask Broadcast(string message, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(message);
+
+ var activeServer = GetActiveController();
+ if (activeServer == null)
+ {
+ Logger.LogInformation("Attempted broadcast failed, no active server!");
+ return false;
+ }
+
+ if (!activeServer.DMApiAvailable)
+ {
+ Logger.LogInformation("Attempted broadcast failed, no DMAPI!");
+ return false;
+ }
+
+ var minimumRequiredVersion = new Version(5, 7, 0);
+ if (activeServer.DMApiVersion < minimumRequiredVersion)
+ {
+ Logger.LogInformation(
+ "Attempted broadcast failed, insufficient interop version: {interopVersion}. Requires {minimumRequiredVersion}!",
+ activeServer.DMApiVersion,
+ minimumRequiredVersion);
+ return false;
+ }
+
+ Logger.LogInformation("Broadcasting: {message}", message);
+
+ var response = await activeServer.SendCommand(
+ TopicParameters.CreateBroadcastParameters(message),
+ cancellationToken);
+
+ return response != null && response.ErrorMessage == null;
+ }
+
///
async ValueTask IEventConsumer.HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken)
{
diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
index ea39745845..fb53f5ab33 100644
--- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
+++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
@@ -148,7 +148,8 @@ namespace Tgstation.Server.Host.Controllers
| DreamDaemonRights.SetVisibility
| DreamDaemonRights.SetProfiler
| DreamDaemonRights.SetLogOutput
- | DreamDaemonRights.SetMapThreads)]
+ | DreamDaemonRights.SetMapThreads
+ | DreamDaemonRights.BroadcastMessage)]
[ProducesResponseType(typeof(DreamDaemonResponse), 200)]
[ProducesResponseType(typeof(ErrorMessageResponse), 410)]
#pragma warning disable CA1502 // TODO: Decomplexify
@@ -208,36 +209,41 @@ namespace Tgstation.Server.Host.Controllers
return false;
}
+ if (CheckModified(x => x.AllowWebClient, DreamDaemonRights.SetWebClient)
+ || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart)
+ || CheckModified(x => x.Port, DreamDaemonRights.SetPort)
+ || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity)
+ || CheckModified(x => x.Visibility, DreamDaemonRights.SetVisibility)
+ || (model.SoftRestart.HasValue && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftRestart))
+ || (model.SoftShutdown.HasValue && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftShutdown))
+ || (model.BroadcastMessage != null && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.BroadcastMessage))
+ || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout)
+ || CheckModified(x => x.HealthCheckSeconds, DreamDaemonRights.SetHealthCheckInterval)
+ || CheckModified(x => x.DumpOnHealthCheckRestart, DreamDaemonRights.CreateDump)
+ || CheckModified(x => x.TopicRequestTimeout, DreamDaemonRights.SetTopicTimeout)
+ || CheckModified(x => x.AdditionalParameters, DreamDaemonRights.SetAdditionalParameters)
+ || CheckModified(x => x.StartProfiler, DreamDaemonRights.SetProfiler)
+ || CheckModified(x => x.LogOutput, DreamDaemonRights.SetLogOutput)
+ || CheckModified(x => x.MapThreads, DreamDaemonRights.SetMapThreads))
+ return Forbid();
+
return await WithComponentInstance(
async instance =>
{
var watchdog = instance.Watchdog;
- var rebootState = watchdog.RebootState;
- var oldSoftRestart = rebootState == RebootState.Restart;
- var oldSoftShutdown = rebootState == RebootState.Shutdown;
-
- if (CheckModified(x => x.AllowWebClient, DreamDaemonRights.SetWebClient)
- || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart)
- || CheckModified(x => x.Port, DreamDaemonRights.SetPort)
- || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity)
- || CheckModified(x => x.Visibility, DreamDaemonRights.SetVisibility)
- || (model.SoftRestart.HasValue && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftRestart))
- || (model.SoftShutdown.HasValue && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftShutdown))
- || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout)
- || CheckModified(x => x.HealthCheckSeconds, DreamDaemonRights.SetHealthCheckInterval)
- || CheckModified(x => x.DumpOnHealthCheckRestart, DreamDaemonRights.CreateDump)
- || CheckModified(x => x.TopicRequestTimeout, DreamDaemonRights.SetTopicTimeout)
- || CheckModified(x => x.AdditionalParameters, DreamDaemonRights.SetAdditionalParameters)
- || CheckModified(x => x.StartProfiler, DreamDaemonRights.SetProfiler)
- || CheckModified(x => x.LogOutput, DreamDaemonRights.SetLogOutput)
- || CheckModified(x => x.MapThreads, DreamDaemonRights.SetMapThreads))
- return Forbid();
+ if (model.BroadcastMessage != null
+ && !await watchdog.Broadcast(model.BroadcastMessage, cancellationToken))
+ return Conflict(new ErrorMessageResponse(ErrorCode.BroadcastFailure));
await DatabaseContext.Save(cancellationToken);
// run this second because current may be modified by it
+ // slight race condition with request cancellation, but I CANNOT be assed right now
await watchdog.ChangeSettings(current, cancellationToken);
+ var rebootState = watchdog.RebootState;
+ var oldSoftRestart = rebootState == RebootState.Restart;
+ var oldSoftShutdown = rebootState == RebootState.Shutdown;
if (!oldSoftRestart && model.SoftRestart == true && watchdog.Status == WatchdogStatus.Online)
await watchdog.Restart(true, cancellationToken);
else if (!oldSoftShutdown && model.SoftShutdown == true)
diff --git a/tests/DMAPI/LongRunning/Test.dm b/tests/DMAPI/LongRunning/Test.dm
index 0a80378b7e..2ee16cc2de 100644
--- a/tests/DMAPI/LongRunning/Test.dm
+++ b/tests/DMAPI/LongRunning/Test.dm
@@ -176,6 +176,10 @@ var/run_bridge_test
if(tactics8)
return received_health_check ? "received health check" : "did not receive health check"
+ var/tactics_broadcast = data["tgs_integration_test_tactics_broadcast"]
+ if(tactics_broadcast)
+ return last_tgs_broadcast || "!!NULL!!"
+
var/legalize_nuclear_bombs = data["shadow_wizard_money_gang"]
if(legalize_nuclear_bombs)
text2file("I expect this to remain here for a while", "kajigger.txt")
diff --git a/tests/DMAPI/test_prelude.dm b/tests/DMAPI/test_prelude.dm
index efb84d46ae..6cb666e372 100644
--- a/tests/DMAPI/test_prelude.dm
+++ b/tests/DMAPI/test_prelude.dm
@@ -3,7 +3,8 @@
#define TGS_READ_GLOBAL(Name) global.##Name
#define TGS_WRITE_GLOBAL(Name, Value) global.##Name = ##Value
#define TGS_PROTECT_DATUM(Path)
-#define TGS_WORLD_ANNOUNCE(message) world << ##message
+var/last_tgs_broadcast
+#define TGS_WORLD_ANNOUNCE(message) if(TRUE) { var/__tgs_announce_message_local = ##message; world << __tgs_announce_message_local; last_tgs_broadcast = __tgs_announce_message_local; }
#define TGS_WARNING_LOG(message) world.log << "Warn: [##message]"
#define TGS_NOTIFY_ADMINS(event)
#define TGS_CLIENT_COUNT 0
diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs
index 5e3a7f8eb0..fcc38a52c3 100644
--- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs
+++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs
@@ -224,12 +224,46 @@ namespace Tgstation.Server.Tests.Live.Instance
await RunTest(false);
}
+ async ValueTask BroadcastTest(CancellationToken cancellationToken)
+ {
+ var topicRequestResult = await topicClient.SendTopic(
+ IPAddress.Loopback,
+ $"tgs_integration_test_tactics_broadcast=1",
+ FindTopicPort(),
+ cancellationToken);
+
+ Assert.IsNotNull(topicRequestResult);
+ Assert.AreEqual("!!NULL!!", topicRequestResult.StringData);
+
+ const string TestBroadcastMessage = "TGS: THIS IS A TEST OF THE EMERGENCY BROADCAST SYSTEM!";
+ await instanceClient.DreamDaemon.Update(new DreamDaemonRequest
+ {
+ BroadcastMessage = TestBroadcastMessage,
+ }, cancellationToken);
+
+ topicRequestResult = await topicClient.SendTopic(
+ IPAddress.Loopback,
+ $"tgs_integration_test_tactics_broadcast=1",
+ FindTopicPort(),
+ cancellationToken);
+
+ Assert.IsNotNull(topicRequestResult);
+ Assert.AreEqual(TestBroadcastMessage, topicRequestResult.StringData);
+ }
+
async Task InteropTestsForLongRunningDme(CancellationToken cancellationToken)
{
await RegressionTest1686(cancellationToken);
+ await ApiAssert.ThrowsException(() => instanceClient.DreamDaemon.Update(new DreamDaemonRequest
+ {
+ BroadcastMessage = "ksjfdksjf",
+ }, cancellationToken), ErrorCode.BroadcastFailure);
+
await StartAndLeaveRunning(cancellationToken);
+ await BroadcastTest(cancellationToken);
+
await RegressionTest1550(cancellationToken);
await TestLegacyBridgeEndpoint(cancellationToken);