diff --git a/README.md b/README.md
index a52beddd1db..c0bd67288f2 100644
--- a/README.md
+++ b/README.md
@@ -137,8 +137,10 @@ See LICENSE-AGPLv3.txt and LICENSE-GPLv3.txt for more details.
tgui clientside is licensed as a subproject under the MIT license.
Font Awesome font files, used by tgui, are licensed under the SIL Open Font License v1.1
tgui assets are licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/).
+The TGS3 API is licensed as a subproject under the MIT license.
See tgui/LICENSE.md for the MIT license.
See tgui/assets/fonts/SIL-OFL-1.1-LICENSE.md for the SIL Open Font License.
+See the footers of code/\_\_DEFINES/server\_tools.dm, code/modules/server\_tools/st\_commands.dm, and code/modules/server\_tools/st\_inteface.dm for the MIT license.
All assets including icons and sound are under a [Creative Commons 3.0 BY-SA license](http://creativecommons.org/licenses/by-sa/3.0/) unless otherwise indicated.
diff --git a/TGS3.json b/TGS3.json
new file mode 100644
index 00000000000..4fd41ab9f1a
--- /dev/null
+++ b/TGS3.json
@@ -0,0 +1,22 @@
+{
+ "documentation": "/tg/station server 3 configuration file",
+ "changelog": {
+ "script": "tools/ss13_genchangelog.py",
+ "arguments": "html/changelog.html html/changelogs",
+ "pip_dependancies": [
+ "PyYaml",
+ "beautifulsoup4"
+ ]
+ },
+ "synchronize_paths": [
+ "html/changelog.html",
+ "html/changelogs/*"
+ ],
+ "static_directories": [
+ "config",
+ "data"
+ ],
+ "dlls": [
+ "libmysql.dll"
+ ]
+ }
\ No newline at end of file
diff --git a/code/__DEFINES/server_tools.dm b/code/__DEFINES/server_tools.dm
index d2be3e578e4..b86a54959f0 100644
--- a/code/__DEFINES/server_tools.dm
+++ b/code/__DEFINES/server_tools.dm
@@ -1,34 +1,123 @@
+// /tg/station 13 server tools API v3.1
+
+//CONFIGURATION
+//use this define if you want to do configuration outside of this file
+#ifndef SERVER_TOOLS_EXTERNAL_CONFIGURATION
+//Comment this out once you've filled in the below
+//#error /tg/station server tools interface unconfigured
+
+//Required interfaces (fill in with your codebase equivalent):
+
+//create a global variable named `Name` and set it to `Value`
+//These globals must not be modifiable from anywhere outside of the server tools
+#define SERVER_TOOLS_DEFINE_AND_SET_GLOBAL(Name, Value) GLOBAL_VAR_INIT(##Name, ##Value); GLOBAL_PROTECT(##Name)
+//Read the value in the global variable `Name`
+#define SERVER_TOOLS_READ_GLOBAL(Name) GLOB.##Name
+//Set the value in the global variable `Name` to `Value`
+#define SERVER_TOOLS_WRITE_GLOBAL(Name, Value) GLOB.##Name = ##Value
+//display an announcement `message` from the server to all players
+#define SERVER_TOOLS_WORLD_ANNOUNCE(message) to_chat(world, "[html_encode(##message)]")
+//Write a string `message` to a server log
+#define SERVER_TOOLS_LOG(message) log_world("SERVICE: [##message]")
+//Notify current in-game administrators of a string `event`
+#define SERVER_TOOLS_NOTIFY_ADMINS(event) message_admins(##event)
+#endif
+
+//Required hooks:
+
+//Put this somewhere in /world/New() that is always run
+#define SERVER_TOOLS_ON_NEW ServiceInit()
+//Put this somewhere in /world/Topic(T, Addr, Master, Keys) that is always run before T is modified
+#define SERVER_TOOLS_ON_TOPIC var/service_topic_return = ServiceCommand(params2list(T)); if(service_topic_return) return service_topic_return
+//Put at the beginning of world/Reboot(reason)
+#define SERVER_TOOLS_ON_REBOOT ServiceReboot()
+
+//Optional callable functions:
+
+//Returns the string version of the API
+#define SERVER_TOOLS_API_VERSION ServiceAPIVersion()
+//Returns TRUE if the world was launched under the server tools and the API matches, FALSE otherwise
+//No function below this succeed if this is FALSE
+#define SERVER_TOOLS_PRESENT RunningService()
+//Gets the current version of the service running the server
+#define SERVER_TOOLS_VERSION ServiceVersion()
+//Forces a hard reboot of BYOND by ending the process
+//unlike del(world) clients will try to reconnect
+//If the service has not requested a shutdown, the world will reboot shortly after
+#define SERVER_TOOLS_REBOOT_BYOND world.ServiceEndProcess()
+/*
+ Gets the list of any testmerged github pull requests
+
+ "[PR Number]" => list(
+ "title" -> PR title
+ "commit" -> Full hash of commit merged
+ "author" -> Github username of the author of the PR
+ )
+*/
+#define SERVER_TOOLS_PR_LIST GetTestMerges()
+//Sends a message to connected game chats
+#define SERVER_TOOLS_CHAT_BROADCAST(message) world.ChatBroadcast(message)
+//Sends a message to connected admin chats
+#define SERVER_TOOLS_RELAY_BROADCAST(message) world.AdminBroadcast(message)
+
+//IMPLEMENTATION
+
+#define SERVICE_API_VERSION_STRING "3.1.0.0"
+
#define REBOOT_MODE_NORMAL 0
#define REBOOT_MODE_HARD 1
#define REBOOT_MODE_SHUTDOWN 2
-#define IRC_STATUS_THROTTLE 5
-
-#define PR_ANNOUNCEMENTS_PER_ROUND 5 //The number of unique PR announcements allowed per round
- //This makes sure that a single person can only spam 3 reopens and 3 closes before being ignored
-
-//keep these in sync with TGS3
#define SERVICE_WORLD_PARAM "server_service"
#define SERVICE_VERSION_PARAM "server_service_version"
#define SERVICE_PR_TEST_JSON "prtestjob.json"
-#define SERVICE_PR_TEST_JSON_OLD "..\\..\\[SERVICE_PR_TEST_JSON]"
+#define SERVICE_INTERFACE_DLL "TGServiceInterface.dll"
+#define SERVICE_INTERFACE_FUNCTION "DDEntryPoint"
#define SERVICE_CMD_HARD_REBOOT "hard_reboot"
#define SERVICE_CMD_GRACEFUL_SHUTDOWN "graceful_shutdown"
#define SERVICE_CMD_WORLD_ANNOUNCE "world_announce"
-#define SERVICE_CMD_IRC_CHECK "irc_check"
-#define SERVICE_CMD_IRC_STATUS "irc_status"
-#define SERVICE_CMD_ADMIN_MSG "adminmsg"
-#define SERVICE_CMD_NAME_CHECK "namecheck"
-#define SERVICE_CMD_ADMIN_WHO "adminwho"
+#define SERVICE_CMD_LIST_CUSTOM "list_custom_commands"
#define SERVICE_CMD_PARAM_KEY "serviceCommsKey"
#define SERVICE_CMD_PARAM_COMMAND "command"
-#define SERVICE_CMD_PARAM_MESSAGE "message"
-#define SERVICE_CMD_PARAM_TARGET "target"
#define SERVICE_CMD_PARAM_SENDER "sender"
+#define SERVICE_CMD_PARAM_CUSTOM "custom"
+#define SERVICE_CMD_API_COMPATIBLE "api_compat"
+
+#define SERVICE_JSON_PARAM_HELPTEXT "help_text"
+#define SERVICE_JSON_PARAM_ADMINONLY "admin_only"
+#define SERVICE_JSON_PARAM_REQUIREDPARAMETERS "required_parameters"
#define SERVICE_REQUEST_KILL_PROCESS "killme"
#define SERVICE_REQUEST_IRC_BROADCAST "irc"
#define SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE "send2irc"
#define SERVICE_REQUEST_WORLD_REBOOT "worldreboot"
+#define SERVICE_REQUEST_API_VERSION "api_ver"
+
+/*
+The MIT License
+
+Copyright (c) 2011 Dominic Tarr
+
+Permission is hereby granted, free of charge,
+to any person obtaining a copy of this software and
+associated documentation files (the "Software"), to
+deal in the Software without restriction, including
+without limitation the rights to use, copy, modify,
+merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom
+the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index 905babe8488..5e190ab7098 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -1364,63 +1364,6 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new)
return FALSE
return TRUE
-//WHATEVER YOU USE THIS FOR MUST BE SANITIZED TO SHIT, IT USES SHELL
-//It also sleeps
-
-//Set this to TRUE before calling
-//This prevents RCEs from badmins
-//kevinz000 if you touch this I will hunt you down
-GLOBAL_VAR_INIT(valid_HTTPSGet, FALSE)
-GLOBAL_PROTECT(valid_HTTPSGet)
-/proc/HTTPSGet(url) //tgs2 support
- if(findtext(url, "\""))
- GLOB.valid_HTTPSGet = FALSE
-
- if(!GLOB.valid_HTTPSGet)
- if(usr)
- CRASH("[usr.ckey]([usr]) just attempted an invalid HTTPSGet on: [url]!")
- else
- CRASH("Invalid HTTPSGet call on: [url]")
- GLOB.valid_HTTPSGet = FALSE
-
- //"This has got to be the ugliest hack I have ever done"
- //warning, here be dragons
- /*
- | @___oo
- /\ /\ / (__,,,,|
- ) /^\) ^\/ _)
- ) /^\/ _)
- ) _ / / _)
- /\ )/\/ || | )_)
- < > |(,,) )__)
- || / \)___)\
- | \____( )___) )___
- \______(_______;;; __;;;
- */
- var/temp_file = "data/HTTPSGetOutput.txt"
- var/command
- if(world.system_type == MS_WINDOWS)
- command = "powershell -Command \"wget [url] -OutFile [temp_file]\""
- else if(world.system_type == UNIX)
- command = "wget -O [temp_file] [url]"
- else
- CRASH("Invalid world.system_type ([world.system_type])? Yell at Lummox.")
-
- log_world("HTTPSGet: [url]")
- var/result = shell(command)
- if(result != 0)
- log_world("Download failed: shell exited with code: [result]")
- return
-
- var/f = file(temp_file)
- if(!f)
- log_world("Download failed: Temp file not found")
- return
-
- . = file2text(f)
- f = null
- fdel(temp_file)
-
#define UNTIL(X) while(!(X)) stoplag()
/proc/pass()
diff --git a/code/controllers/configuration/entries/config.dm b/code/controllers/configuration/entries/config.dm
index cabb6dca27f..ed09ca4a9c5 100644
--- a/code/controllers/configuration/entries/config.dm
+++ b/code/controllers/configuration/entries/config.dm
@@ -227,9 +227,6 @@ CONFIG_DEF(flag/load_jobs_from_txt)
CONFIG_DEF(flag/forbid_singulo_possession)
-CONFIG_DEF(flag/useircbot) //tgs2 support
- protection = CONFIG_ENTRY_LOCKED
-
CONFIG_DEF(flag/automute_on) //enables automuting/spam prevention
CONFIG_DEF(string/panic_server_name)
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index 6b17522ebe0..be5bedce6a4 100755
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -87,6 +87,8 @@ SUBSYSTEM_DEF(ticker)
for(var/client/C in GLOB.clients)
window_flash(C, ignorepref = TRUE) //let them know lobby has opened up.
to_chat(world, "Welcome to [station_name()]!")
+ if(CONFIG_GET(flag/irc_announce_new_game))
+ SERVER_TOOLS_CHAT_BROADCAST("New round starting on [SSmapping.config.map_name]!")
current_state = GAME_STATE_PREGAME
//Everyone who wants to be an observer is now spawned
create_observers()
diff --git a/code/datums/helper_datums/getrev.dm b/code/datums/helper_datums/getrev.dm
index 4bee4c2c094..dfaa6c752a6 100644
--- a/code/datums/helper_datums/getrev.dm
+++ b/code/datums/helper_datums/getrev.dm
@@ -2,25 +2,10 @@
var/originmastercommit
var/commit
var/list/testmerge = list()
- var/has_pr_details = FALSE //tgs2 support
var/date
/datum/getrev/New()
- if(world.RunningService())
- var/file_name
- if(ServiceVersion()) //will return null for versions < 3.0.91.0
- file_name = SERVICE_PR_TEST_JSON
- else
- file_name = SERVICE_PR_TEST_JSON_OLD
- if(fexists(file_name))
- testmerge = json_decode(file2text(file_name))
-#ifdef SERVERTOOLS
- else if(!world.RunningService() && fexists("../prtestjob.lk")) //tgs2 support
- var/list/tmp = world.file2list("..\\prtestjob.lk")
- for(var/I in tmp)
- if(I)
- testmerge |= I
-#endif
+ testmerge = SERVER_TOOLS_PR_LIST
log_world("Running /tg/ revision:")
var/list/logs = world.file2list(".git/logs/HEAD")
if(logs)
@@ -36,53 +21,20 @@
log_world(commit)
for(var/line in testmerge)
if(line)
- if(world.RunningService())
- var/tmcommit = testmerge[line]["commit"]
- log_world("Test merge active of PR #[line] commit [tmcommit]")
- SSblackbox.add_details("testmerged_prs","[line]|[tmcommit]")
- else //tgs2 support
- log_world("Test merge active of PR #[line]")
- SSblackbox.add_details("testmerged_prs","[line]")
+ var/tmcommit = testmerge[line]["commit"]
+ log_world("Test merge active of PR #[line] commit [tmcommit]")
+ SSblackbox.add_details("testmerged_prs","[line]|[tmcommit]")
log_world("Based off origin/master commit [originmastercommit]")
else
log_world(originmastercommit)
-/datum/getrev/proc/DownloadPRDetails()
- var/repo_id = CONFIG_GET(number/githubrepoid)
- if(!repo_id)
- if(testmerge.len)
- log_world("PR details download failed: No github repo config set")
- return
- for(var/line in testmerge)
- if(!isnum(text2num(line)))
- log_world("PR details download failed: Invalid PR number: [line]")
- return
-
- var/url = "https://api.github.com/repositories/[repo_id]/pulls/[line].json"
- GLOB.valid_HTTPSGet = TRUE
- var/json = HTTPSGet(url)
- if(!json)
- return
-
- testmerge[line] = json_decode(json)
-
- if(!testmerge[line])
- log_world("PR details download failed: null details returned")
- return
- CHECK_TICK
- log_world("PR details successfully downloaded")
- has_pr_details = TRUE
/datum/getrev/proc/GetTestMergeInfo(header = TRUE)
if(!testmerge.len)
return ""
. = header ? "The following pull requests are currently test merged:
" : ""
for(var/line in testmerge)
- var/details
- if(world.RunningService())
- var/cm = testmerge[line]["commit"]
- details = ": '" + html_encode(testmerge[line]["title"]) + "' by " + html_encode(testmerge[line]["author"]) + " at commit " + html_encode(copytext(cm, 1, min(length(cm), 7)))
- else if(has_pr_details) //tgs2 support
- details = ": '" + html_encode(testmerge[line]["title"]) + "' by " + html_encode(testmerge[line]["user"]["login"])
+ var/cm = testmerge[line]["commit"]
+ var/details = ": '" + html_encode(testmerge[line]["title"]) + "' by " + html_encode(testmerge[line]["author"]) + " at commit " + html_encode(copytext(cm, 1, min(length(cm), 7)))
if(details && findtext(details, "\[s\]") && (!usr || !usr.client.holder))
continue
. += "#[line][details]
"
diff --git a/code/game/world.dm b/code/game/world.dm
index 316ed709617..65490a9a969 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -1,3 +1,6 @@
+#define PR_ANNOUNCEMENTS_PER_ROUND 5 //The number of unique PR announcements allowed per round
+ //This makes sure that a single person can only spam 3 reopens and 3 closes before being ignored
+
GLOBAL_VAR(security_mode)
GLOBAL_PROTECT(security_mode)
@@ -19,8 +22,7 @@ GLOBAL_PROTECT(security_mode)
SetupLogs()
- if(!RunningService()) //tgs2 support
- GLOB.revdata.DownloadPRDetails()
+ SERVER_TOOLS_ON_NEW
load_motd()
load_admins()
@@ -33,9 +35,6 @@ GLOBAL_PROTECT(security_mode)
Master.Initialize(10, FALSE)
- if(CONFIG_GET(flag/irc_announce_new_game))
- IRCBroadcast("New round starting on [SSmapping.config.map_name]!")
-
/world/proc/SetupExternalRSC()
#if (PRELOAD_RSC == 0)
external_rsc_urls = world.file2list("config/external_rsc_urls.txt","\n")
@@ -125,8 +124,8 @@ GLOBAL_PROTECT(security_mode)
if(!pinging && !playing && config && CONFIG_GET(flag/log_world_topic))
WRITE_FILE(GLOB.world_game_log, "TOPIC: \"[T]\", from:[addr], master:[master], key:[key]")
- if(input[SERVICE_CMD_PARAM_KEY])
- return ServiceCommand(input)
+ SERVER_TOOLS_ON_TOPIC //redirect to server tools if necessary
+
var/comms_key = CONFIG_GET(string/comms_key)
var/key_valid = (comms_key && input["key"] == comms_key)
@@ -143,17 +142,6 @@ GLOBAL_PROTECT(security_mode)
n++
return n
- else if("ircstatus" in input) //tgs2 support
- var/static/last_irc_status = 0
- if(world.time - last_irc_status < 50)
- return
- var/list/adm = get_admin_counts()
- var/list/allmins = adm["total"]
- var/status = "Admins: [allmins.len] (Active: [english_list(adm["present"])] AFK: [english_list(adm["afk"])] Stealth: [english_list(adm["stealth"])] Skipped: [english_list(adm["noflags"])]). "
- status += "Players: [GLOB.clients.len] (Active: [get_active_player_count(0,1,0)]). Mode: [SSticker.mode.name]."
- send2irc("Status", status)
- last_irc_status = world.time
-
else if("status" in input)
var/list/s = list()
s["version"] = GLOB.game_version
@@ -211,24 +199,6 @@ GLOBAL_PROTECT(security_mode)
if(input["crossmessage"] == "News_Report")
minor_announce(input["message"], "Breaking Update From [input["message_sender"]]")
- else if("adminmsg" in input) //tgs2 support
- if(!key_valid)
- return "Bad Key"
- else
- return IrcPm(input["adminmsg"],input["msg"],input["sender"])
-
- else if("namecheck" in input) //tgs2 support
- if(!key_valid)
- return "Bad Key"
- else
- log_admin("IRC Name Check: [input["sender"]] on [input["namecheck"]]")
- message_admins("IRC name checking on [input["namecheck"]] from [input["sender"]]")
- return keywords_lookup(input["namecheck"],1)
- else if("adminwho" in input) //tgs2 support
- if(!key_valid)
- return "Bad Key"
- else
- return ircadminwho()
else if("server_hop" in input)
show_server_hop_transfer_screen(input["server_hop"])
@@ -247,7 +217,7 @@ GLOBAL_PROTECT(security_mode)
C.AnnouncePR(final_composed)
/world/Reboot(reason = 0, fast_track = FALSE)
- ServiceReboot() //handles alternative actions if necessary
+ SERVER_TOOLS_ON_REBOOT
if (reason || fast_track) //special reboot, do none of the normal stuff
if (usr)
log_admin("[key_name(usr)] Has requested an immediate world restart via client side debugging tools")
diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm
index 881e59204d9..a64cd8f40ea 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -421,8 +421,8 @@
return
var/list/options = list("Regular Restart", "Hard Restart (No Delay/Feeback Reason)", "Hardest Restart (No actions, just reboot)")
- if(world.RunningService())
- options += "Service Restart (Force restart DD)";
+ if(SERVER_TOOLS_PRESENT)
+ options += "Server Restart (Kill and restart DD)";
var/rebootconfirm
if(SSticker.admin_delay_notice)
@@ -434,16 +434,19 @@
var/result = input(usr, "Select reboot method", "World Reboot", options[1]) as null|anything in options
if(result)
SSblackbox.add_details("admin_verb","Reboot World") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
+ var/init_by = "Initiated by [usr.client.holder.fakekey ? "Admin" : usr.key]."
switch(result)
if("Regular Restart")
- SSticker.Reboot("Initiated by [usr.client.holder.fakekey ? "Admin" : usr.key].", "admin reboot - by [usr.key] [usr.client.holder.fakekey ? "(stealth)" : ""]", 10)
+ SSticker.Reboot(init_by, "admin reboot - by [usr.key] [usr.client.holder.fakekey ? "(stealth)" : ""]", 10)
if("Hard Restart (No Delay, No Feeback Reason)")
+ to_chat(world, "World reboot - [init_by]")
world.Reboot()
if("Hardest Restart (No actions, just reboot)")
+ to_chat(world, "Hard world reboot - [init_by]")
world.Reboot(fast_track = TRUE)
- if("Service Restart (Force restart DD)")
- GLOB.reboot_mode = REBOOT_MODE_HARD
- world.ServiceReboot()
+ if("Server Restart (Kill and restart DD)")
+ to_chat(world, "Server restart - [init_by]")
+ SERVER_TOOLS_REBOOT_BYOND
/datum/admins/proc/end_round()
set category = "Server"
diff --git a/code/modules/admin/chat_commands.dm b/code/modules/admin/chat_commands.dm
new file mode 100644
index 00000000000..e0d7f1315e8
--- /dev/null
+++ b/code/modules/admin/chat_commands.dm
@@ -0,0 +1,63 @@
+#define IRC_STATUS_THROTTLE 5
+
+/datum/server_tools_command/ircstatus
+ name = "status"
+ help_text = "Gets the admincount, playercount, gamemode, and true game mode of the server"
+ admin_only = TRUE
+ var/static/last_irc_status = 0
+
+/datum/server_tools_command/ircstatus/Run(sender, params)
+ var/rtod = REALTIMEOFDAY
+ if(rtod - last_irc_status < IRC_STATUS_THROTTLE)
+ return
+ last_irc_status = rtod
+ var/list/adm = get_admin_counts()
+ var/list/allmins = adm["total"]
+ var/status = "Admins: [allmins.len] (Active: [english_list(adm["present"])] AFK: [english_list(adm["afk"])] Stealth: [english_list(adm["stealth"])] Skipped: [english_list(adm["noflags"])]). "
+ status += "Players: [GLOB.clients.len] (Active: [get_active_player_count(0,1,0)]). Mode: [SSticker.mode ? SSticker.mode.name : "Not started"]."
+ return status
+
+/datum/server_tools_command/irccheck
+ name = "check"
+ help_text = "Gets the playercount, gamemode, and address of the server"
+ admin_only = TRUE
+ var/static/last_irc_check = 0
+
+/datum/server_tools_command/irccheck/Run(sender, params)
+ var/rtod = REALTIMEOFDAY
+ if(rtod - last_irc_check < IRC_STATUS_THROTTLE)
+ return
+ last_irc_check = rtod
+ var/server = CONFIG_GET(string/server)
+ return "[GLOB.round_id ? "Round #[GLOB.round_id]: " : ""][GLOB.clients.len] players on [SSmapping.config.map_name], Mode: [GLOB.master_mode]; Round [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active" : "Finishing") : "Starting"] -- [server ? server : "[world.internet_address]:[world.port]"]"
+
+/datum/server_tools_command/ahelp
+ name = "ahelp"
+ help_text = " |list>>"
+ required_parameters = 2
+ admin_only = TRUE
+
+/datum/server_tools_command/ahelp/Run(sender, params)
+ var/list/all_params = splittext(params, " ")
+ var/target = all_params[1]
+ all_params.Cut(1, 2)
+ return IrcPm(target, all_params.Join(" "), sender)
+
+/datum/server_tools_command/namecheck
+ name = "namecheck"
+ help_text = "Returns info on the specified target"
+ required_parameters = 1
+ admin_only = TRUE
+
+/datum/server_tools_command/namecheck/Run(sender, params)
+ log_admin("IRC Name Check: [sender] on [params]")
+ message_admins("IRC name checking on [params] from [sender]")
+ return keywords_lookup(params, 1)
+
+/datum/server_tools_command/adminwho
+ name = "adminwho"
+ help_text = "Lists administrators currently on the server"
+ admin_only = TRUE
+
+/datum/server_tools_command/adminwho/Run(sender, params)
+ return ircadminwho()
diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm
index 25cfdba6f06..aa6c462d1f2 100644
--- a/code/modules/admin/verbs/adminhelp.dm
+++ b/code/modules/admin/verbs/adminhelp.dm
@@ -587,10 +587,8 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
/proc/send2irc(msg,msg2)
- if(world.RunningService())
- world.ExportService("[SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE] [msg] | [msg2]")
- else if(CONFIG_GET(flag/useircbot))
- shell("python nudge.py [msg] [msg2]")
+ if(SERVER_TOOLS_PRESENT)
+ SERVER_TOOLS_RELAY_BROADCAST("[msg] | [msg2]")
/proc/send2otherserver(source,msg,type = "Ahelp")
var/comms_key = CONFIG_GET(string/comms_key)
diff --git a/code/modules/server_tools/nudge.py b/code/modules/server_tools/nudge.py
deleted file mode 100644
index 4652d178cba..00000000000
--- a/code/modules/server_tools/nudge.py
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env python3
-import sys
-import pickle
-import socket
-
-
-def pack():
- data = sys.argv[1]
-
- nudge(str.encode(data))
-
-
-def nudge(data):
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- with open('config/server_to_tool_bridge_port.txt', 'r') as myfile:
- portstr=myfile.read().replace('\n', '').strip()
- s.connect(("localhost", int(portstr)))
- s.send(data)
- s.close()
-
-if __name__ == "__main__" and len(sys.argv) > 1:
- pack()
diff --git a/code/modules/server_tools/server_tools.dm b/code/modules/server_tools/server_tools.dm
deleted file mode 100644
index 9cacb4542f2..00000000000
--- a/code/modules/server_tools/server_tools.dm
+++ /dev/null
@@ -1,93 +0,0 @@
-GLOBAL_VAR_INIT(reboot_mode, REBOOT_MODE_NORMAL) //if the world should request the service to kill it at reboot
-GLOBAL_PROTECT(reboot_mode)
-
-/world/proc/RunningService()
- return params[SERVICE_WORLD_PARAM]
-
-/proc/ServiceVersion()
- if(world.RunningService())
- return world.params[SERVICE_VERSION_PARAM]
-
-/world/proc/ExportService(command)
- return RunningService() && shell("python code/modules/server_tools/nudge.py \"[command]\"") == 0
-
-/world/proc/IRCBroadcast(msg)
- ExportService("[SERVICE_REQUEST_IRC_BROADCAST] [msg]")
-
-/world/proc/ServiceEndProcess()
- log_world("Sending shutdown request!");
- sleep(1) //flush the buffers
- ExportService(SERVICE_REQUEST_KILL_PROCESS)
-
-//called at the exact moment the world is supposed to reboot
-/world/proc/ServiceReboot()
- switch(GLOB.reboot_mode)
- if(REBOOT_MODE_HARD)
- to_chat(src, "Hard reboot triggered, you will automatically reconnect...")
- ServiceEndProcess()
- if(REBOOT_MODE_SHUTDOWN)
- to_chat(src, "The server is shutting down...")
- ServiceEndProcess()
- else
- ExportService(SERVICE_REQUEST_WORLD_REBOOT) //just let em know
-
-/world/proc/ServiceCommand(list/params)
- var/sCK = RunningService()
- var/their_sCK = params[SERVICE_CMD_PARAM_KEY]
-
- if(!their_sCK || their_sCK != sCK)
- return "Invalid comms key!";
-
- var/command = params[SERVICE_CMD_PARAM_COMMAND]
- if(!command)
- return "No command!"
-
- var/static/last_irc_status = 0
- switch(command)
- if(SERVICE_CMD_HARD_REBOOT)
- if(GLOB.reboot_mode != REBOOT_MODE_HARD)
- GLOB.reboot_mode = REBOOT_MODE_HARD
- log_world("Hard reboot requested by service")
- message_admins("The world will hard reboot at the end of the game. Requested by service.")
- SSblackbox.set_val("service_hard_restart", TRUE)
- if(SERVICE_CMD_GRACEFUL_SHUTDOWN)
- if(GLOB.reboot_mode != REBOOT_MODE_SHUTDOWN)
- GLOB.reboot_mode = REBOOT_MODE_SHUTDOWN
- log_world("Shutdown requested by service")
- message_admins("The world will shutdown at the end of the game. Requested by service.")
- SSblackbox.set_val("service_shutdown", TRUE)
- if(SERVICE_CMD_WORLD_ANNOUNCE)
- var/msg = params["message"]
- if(!istext(msg) || !msg)
- return "No message set!"
- to_chat(src, "[html_encode(msg)]")
- return "SUCCESS"
- if(SERVICE_CMD_IRC_STATUS)
- var/rtod = REALTIMEOFDAY
- if(rtod - last_irc_status < IRC_STATUS_THROTTLE)
- return
- last_irc_status = rtod
- var/list/adm = get_admin_counts()
- var/list/allmins = adm["total"]
- var/status = "Admins: [allmins.len] (Active: [english_list(adm["present"])] AFK: [english_list(adm["afk"])] Stealth: [english_list(adm["stealth"])] Skipped: [english_list(adm["noflags"])]). "
- status += "Players: [GLOB.clients.len] (Active: [get_active_player_count(0,1,0)]). Mode: [SSticker.mode ? SSticker.mode.name : "Not started"]."
- return status
- if(SERVICE_CMD_IRC_CHECK)
- var/rtod = REALTIMEOFDAY
- if(rtod - last_irc_status < IRC_STATUS_THROTTLE)
- return
- last_irc_status = rtod
- var/config_server = CONFIG_GET(string/server)
- return "[GLOB.round_id ? "Round #[GLOB.round_id]: " : ""][GLOB.clients.len] players on [SSmapping.config.map_name], Mode: [GLOB.master_mode]; Round [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active" : "Finishing") : "Starting"] -- [config_server ? config_server : "[world.internet_address]:[world.port]"]"
- if(SERVICE_CMD_ADMIN_MSG)
- return IrcPm(params[SERVICE_CMD_PARAM_TARGET], params[SERVICE_CMD_PARAM_MESSAGE], params[SERVICE_CMD_PARAM_SENDER])
-
- if(SERVICE_CMD_NAME_CHECK)
- log_admin("IRC Name Check: [params[SERVICE_CMD_PARAM_SENDER]] on [params[SERVICE_CMD_PARAM_TARGET]]")
- message_admins("IRC name checking on [params[SERVICE_CMD_PARAM_TARGET]] from [params[SERVICE_CMD_PARAM_SENDER]]")
- return keywords_lookup(params[SERVICE_CMD_PARAM_TARGET], 1)
- if(SERVICE_CMD_ADMIN_WHO)
- return ircadminwho()
- else
- return "Unknown command: [command]"
-
diff --git a/code/modules/server_tools/st_commands.dm b/code/modules/server_tools/st_commands.dm
new file mode 100644
index 00000000000..9ec87a595c6
--- /dev/null
+++ b/code/modules/server_tools/st_commands.dm
@@ -0,0 +1,76 @@
+/datum/server_tools_command
+ var/name = "" //the string to trigger this command on a chat bot. e.g. TGS3_BOT: do_this_command
+ var/help_text = "" //help text for this command
+ var/required_parameters = 0 //number of parameters required for this command
+ var/admin_only = FALSE //set to TRUE if this command should only be usable by registered chat admins
+
+//override to implement command
+//sender is the display name of who sent the command
+//params is the trimmed string following the command name
+/datum/server_tools_command/proc/Run(sender, params)
+ CRASH("[type] has no implementation for Run()")
+
+/world/proc/ListServiceCustomCommands(warnings_only)
+ if(!warnings_only)
+ . = list()
+ var/list/command_name_types = list()
+ var/list/warned_command_names = warnings_only ? list() : null
+ for(var/I in typesof(/datum/server_tools_command) - /datum/server_tools_command)
+ var/datum/server_tools_command/stc = I
+ var/command_name = initial(stc.name)
+ var/static/list/warned_server_tools_names = list()
+ if(!command_name || findtext(command_name, " ") || findtext(command_name, "'") || findtext(command_name, "\""))
+ if(warnings_only && !warned_command_names[command_name])
+ SERVER_TOOLS_LOG("WARNING: Custom command [command_name] can't be used as it is empty or contains illegal characters!")
+ warned_command_names[command_name] = TRUE
+ continue
+
+ if(command_name_types[command_name])
+ if(warnings_only)
+ SERVER_TOOLS_LOG("WARNING: Custom commands [command_name_types[command_name]] and [stc] have the same name, only [command_name_types[command_name]] will be available!")
+ continue
+ command_name_types[stc] = command_name
+
+ if(!warnings_only)
+ .[command_name] = list(SERVICE_JSON_PARAM_HELPTEXT = initial(stc.help_text), SERVICE_JSON_PARAM_ADMINONLY = initial(stc.admin_only), SERVICE_JSON_PARAM_REQUIREDPARAMETERS = initial(stc.required_parameters))
+
+/world/proc/HandleServiceCustomCommand(command, sender, params)
+ var/static/list/cached_custom_server_tools_commands
+ if(!cached_custom_server_tools_commands)
+ cached_custom_server_tools_commands = list()
+ for(var/I in typesof(/datum/server_tools_command) - /datum/server_tools_command)
+ var/datum/server_tools_command/stc = I
+ cached_custom_server_tools_commands[lowertext(initial(stc.name))] = stc
+
+ var/command_type = cached_custom_server_tools_commands[command]
+ if(!command_type)
+ return FALSE
+ var/datum/server_tools_command/stc = new command_type
+ return stc.Run(sender, params) || TRUE
+
+/*
+The MIT License
+
+Copyright (c) 2011 Dominic Tarr
+
+Permission is hereby granted, free of charge,
+to any person obtaining a copy of this software and
+associated documentation files (the "Software"), to
+deal in the Software without restriction, including
+without limitation the rights to use, copy, modify,
+merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom
+the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
diff --git a/code/modules/server_tools/st_interface.dm b/code/modules/server_tools/st_interface.dm
new file mode 100644
index 00000000000..12fb5a85c28
--- /dev/null
+++ b/code/modules/server_tools/st_interface.dm
@@ -0,0 +1,125 @@
+SERVER_TOOLS_DEFINE_AND_SET_GLOBAL(reboot_mode, REBOOT_MODE_NORMAL)
+SERVER_TOOLS_DEFINE_AND_SET_GLOBAL(server_tools_api_compatible, FALSE)
+
+/proc/GetTestMerges()
+ if(RunningService() && fexists(SERVICE_PR_TEST_JSON))
+ . = json_decode(file2text(SERVICE_PR_TEST_JSON))
+ if(.)
+ return
+ return list()
+
+/world/proc/ServiceInit()
+ if(!RunningService(TRUE))
+ return
+ ListServiceCustomCommands(TRUE)
+ ExportService("[SERVICE_REQUEST_API_VERSION] [SERVER_TOOLS_API_VERSION]", TRUE)
+
+/proc/RunningService(skip_compat_check = FALSE)
+ return (skip_compat_check || SERVER_TOOLS_READ_GLOBAL(server_tools_api_compatible)) && world.params[SERVICE_WORLD_PARAM] != null
+
+/proc/ServiceVersion()
+ if(RunningService(TRUE))
+ return world.params[SERVICE_VERSION_PARAM]
+
+/proc/ServiceAPIVersion()
+ return SERVICE_API_VERSION_STRING
+
+/world/proc/ExportService(command, skip_compat_check = FALSE)
+ . = FALSE
+ if(!RunningService(skip_compat_check))
+ return
+ if(skip_compat_check && !fexists(SERVICE_INTERFACE_DLL))
+ CRASH("Service parameter present but no interface DLL detected. This is symptomatic of running a service less than version 3.1! Please upgrade.")
+ call(SERVICE_INTERFACE_DLL, SERVICE_INTERFACE_FUNCTION)(command) //trust no retval
+ return TRUE
+
+/world/proc/ChatBroadcast(message)
+ ExportService("[SERVICE_REQUEST_IRC_BROADCAST] [message]")
+
+/world/proc/AdminBroadcast(message)
+ ExportService("[SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE] [message]")
+
+/world/proc/ServiceEndProcess()
+ SERVER_TOOLS_LOG("Sending shutdown request!");
+ sleep(world.tick_lag) //flush the buffers
+ ExportService(SERVICE_REQUEST_KILL_PROCESS)
+
+//called at the exact moment the world is supposed to reboot
+/world/proc/ServiceReboot()
+ switch(SERVER_TOOLS_READ_GLOBAL(reboot_mode))
+ if(REBOOT_MODE_HARD)
+ SERVER_TOOLS_WORLD_ANNOUNCE("Hard reboot triggered, you will automatically reconnect...")
+ ServiceEndProcess()
+ if(REBOOT_MODE_SHUTDOWN)
+ SERVER_TOOLS_WORLD_ANNOUNCE("The server is shutting down...")
+ ServiceEndProcess()
+ else
+ ExportService(SERVICE_REQUEST_WORLD_REBOOT) //just let em know
+
+/world/proc/ServiceCommand(list/params)
+ var/their_sCK = params[SERVICE_CMD_PARAM_KEY]
+ if(!their_sCK || !RunningService(TRUE))
+ return FALSE //continue world/Topic
+
+ var/sCK = world.params[SERVICE_WORLD_PARAM]
+ if(their_sCK != sCK)
+ return "Invalid comms key!";
+
+ var/command = params[SERVICE_CMD_PARAM_COMMAND]
+ if(!command)
+ return "No command!"
+
+ switch(command)
+ if(SERVICE_CMD_API_COMPATIBLE)
+ SERVER_TOOLS_WRITE_GLOBAL(server_tools_api_compatible, TRUE)
+ return "SUCCESS"
+ if(SERVICE_CMD_HARD_REBOOT)
+ if(SERVER_TOOLS_READ_GLOBAL(reboot_mode) != REBOOT_MODE_HARD)
+ SERVER_TOOLS_WRITE_GLOBAL(reboot_mode, REBOOT_MODE_HARD)
+ SERVER_TOOLS_LOG("Hard reboot requested by service")
+ SERVER_TOOLS_NOTIFY_ADMINS("The world will hard reboot at the end of the game. Requested by service.")
+ if(SERVICE_CMD_GRACEFUL_SHUTDOWN)
+ if(SERVER_TOOLS_READ_GLOBAL(reboot_mode) != REBOOT_MODE_SHUTDOWN)
+ SERVER_TOOLS_WRITE_GLOBAL(reboot_mode, REBOOT_MODE_SHUTDOWN)
+ SERVER_TOOLS_LOG("Shutdown requested by service")
+ message_admins("The world will shutdown at the end of the game. Requested by service.")
+ if(SERVICE_CMD_WORLD_ANNOUNCE)
+ var/msg = params["message"]
+ if(!istext(msg) || !msg)
+ return "No message set!"
+ SERVER_TOOLS_WORLD_ANNOUNCE(msg)
+ return "SUCCESS"
+ if(SERVICE_CMD_LIST_CUSTOM)
+ return json_encode(ListServiceCustomCommands(FALSE))
+ else
+ var/custom_command_result = HandleServiceCustomCommand(lowertext(command), params[SERVICE_CMD_PARAM_SENDER], params[SERVICE_CMD_PARAM_CUSTOM])
+ if(custom_command_result)
+ return istext(custom_command_result) ? custom_command_result : "SUCCESS"
+ return "Unknown command: [command]"
+
+/*
+The MIT License
+
+Copyright (c) 2011 Dominic Tarr
+
+Permission is hereby granted, free of charge,
+to any person obtaining a copy of this software and
+associated documentation files (the "Software"), to
+deal in the Software without restriction, including
+without limitation the rights to use, copy, modify,
+merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom
+the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
diff --git a/config/server_to_tool_bridge_port.txt b/config/server_to_tool_bridge_port.txt
deleted file mode 100644
index 75fd9c9d345..00000000000
--- a/config/server_to_tool_bridge_port.txt
+++ /dev/null
@@ -1 +0,0 @@
-45678
\ No newline at end of file
diff --git a/tgstation.dme b/tgstation.dme
index 7b8fd7797d3..d9d5c95e27b 100755
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1017,6 +1017,7 @@
#include "code\modules\admin\admin_verbs.dm"
#include "code\modules\admin\adminmenu.dm"
#include "code\modules\admin\banjob.dm"
+#include "code\modules\admin\chat_commands.dm"
#include "code\modules\admin\create_mob.dm"
#include "code\modules\admin\create_object.dm"
#include "code\modules\admin\create_poll.dm"
@@ -2067,7 +2068,8 @@
#include "code\modules\ruins\spaceruin_code\TheDerelict.dm"
#include "code\modules\security_levels\keycard_authentication.dm"
#include "code\modules\security_levels\security_levels.dm"
-#include "code\modules\server_tools\server_tools.dm"
+#include "code\modules\server_tools\st_interface.dm"
+#include "code\modules\server_tools\st_commands.dm"
#include "code\modules\shuttle\arrivals.dm"
#include "code\modules\shuttle\assault_pod.dm"
#include "code\modules\shuttle\computer.dm"