code:
/world/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE)
	var/current_api = TGS_READ_GLOBAL(tgs)
	if(current_api)
		TGS_ERROR_LOG("API datum already set (\ref[current_api] ([current_api]))! Was TgsNew() called more than once?")
		return

	if(!(minimum_required_security_level in list(TGS_SECURITY_ULTRASAFE, TGS_SECURITY_SAFE, TGS_SECURITY_TRUSTED)))
		TGS_ERROR_LOG("Invalid minimum_required_security_level: [minimum_required_security_level]!")
		return

#ifdef TGS_V3_API
	if(minimum_required_security_level != TGS_SECURITY_TRUSTED)
		TGS_WARNING_LOG("V3 DMAPI requires trusted security!")
		minimum_required_security_level = TGS_SECURITY_TRUSTED
#endif
	var/raw_parameter = world.params[TGS_VERSION_PARAMETER]
	if(!raw_parameter)
		return

	var/datum/tgs_version/version = new(raw_parameter)
	if(!version.Valid(FALSE))
		TGS_ERROR_LOG("Failed to validate DMAPI version parameter: [raw_parameter]!")
		return

	var/api_datum
	switch(version.suite)
		if(3)
#ifndef TGS_V3_API
			TGS_ERROR_LOG("Detected V3 API but TGS_V3_API isn't defined!")
			return
#else
			switch(version.minor)
				if(2)
					api_datum = /datum/tgs_api/v3210
#endif
		if(4)
			switch(version.minor)
				if(0)
					api_datum = /datum/tgs_api/v4
		if(5)
			api_datum = /datum/tgs_api/v5

	var/datum/tgs_version/max_api_version = TgsMaximumApiVersion();
	if(version.suite != null && version.minor != null && version.patch != null && version.deprecated_patch != null && version.deprefixed_parameter > max_api_version.deprefixed_parameter)
		TGS_ERROR_LOG("Detected unknown API version! Defaulting to latest. Update the DMAPI to fix this problem.")
		api_datum = /datum/tgs_api/latest

	if(!api_datum)
		TGS_ERROR_LOG("Found unsupported API version: [raw_parameter]. If this is a valid version please report this, backporting is done on demand.")
		return

	TGS_INFO_LOG("Activating API for version [version.deprefixed_parameter]")

	if(event_handler && !istype(event_handler))
		TGS_ERROR_LOG("Invalid parameter for event_handler: [event_handler]")
		event_handler = null

	var/datum/tgs_api/new_api = new api_datum(event_handler, version)

	TGS_WRITE_GLOBAL(tgs, new_api)

	var/result = new_api.OnWorldNew(minimum_required_security_level)
	if(!result || result == TGS_UNIMPLEMENTED)
		TGS_WRITE_GLOBAL(tgs, null)
		TGS_ERROR_LOG("Failed to activate API!")

/world/TgsMaximumApiVersion()
	return new /datum/tgs_version("5.x.x")

/world/TgsMinimumApiVersion()
	return new /datum/tgs_version("3.2.x")

/world/TgsInitializationComplete()
	var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
	if(api)
		api.OnInitializationComplete()

/world/proc/TgsTopic(T)
	var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
	if(api)
		var/result = api.OnTopic(T)
		if(result != TGS_UNIMPLEMENTED)
			return result

/world/TgsRevision()
	var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
	if(api)
		var/result = api.Revision()
		if(result != TGS_UNIMPLEMENTED)
			return result

/world/TgsReboot()
	var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
	if(api)
		api.OnReboot()

/world/TgsAvailable()
	return TGS_READ_GLOBAL(tgs) != null

/world/TgsVersion()
	var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
	if(api)
		return api.version

/world/TgsApiVersion()
	var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
	if(api)
		return api.ApiVersion()

/world/TgsInstanceName()
	var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
	if(api)
		var/result = api.InstanceName()
		if(result != TGS_UNIMPLEMENTED)
			return result

/world/TgsTestMerges()
	var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
	if(api)
		var/result = api.TestMerges()
		if(result != TGS_UNIMPLEMENTED)
			return result
	return list()

/world/TgsEndProcess()
	var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
	if(api)
		api.EndProcess()

/world/TgsChatChannelInfo()
	var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
	if(api)
		var/result = api.ChatChannelInfo()
		if(result != TGS_UNIMPLEMENTED)
			return result
	return list()

/world/TgsChatBroadcast(message, list/channels)
	var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
	if(api)
		api.ChatBroadcast(message, channels)

/world/TgsTargetedChatBroadcast(message, admin_only)
	var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
	if(api)
		api.ChatTargetedBroadcast(message, admin_only)

/world/TgsChatPrivateMessage(message, datum/tgs_chat_user/user)
	var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
	if(api)
		api.ChatPrivateMessage(message, user)

/world/TgsSecurityLevel()
	var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
	if(api)
		api.SecurityLevel()


code:
TGS_DEFINE_AND_SET_GLOBAL(tgs, null)

/datum/tgs_api
	var/datum/tgs_version/version
	var/datum/tgs_event_handler/event_handler

/datum/tgs_api/New(datum/tgs_event_handler/event_handler, datum/tgs_version/version)
	. = ..()
	src.event_handler = event_handler
	src.version = version

/datum/tgs_api/latest
	parent_type = /datum/tgs_api/v5

TGS_PROTECT_DATUM(/datum/tgs_api)

/datum/tgs_api/proc/ApiVersion()
	return TGS_UNIMPLEMENTED

/datum/tgs_api/proc/OnWorldNew(datum/tgs_event_handler/event_handler)
	return TGS_UNIMPLEMENTED

/datum/tgs_api/proc/OnInitializationComplete()
	return TGS_UNIMPLEMENTED

/datum/tgs_api/proc/OnTopic(T)
	return TGS_UNIMPLEMENTED

/datum/tgs_api/proc/OnReboot()
	return TGS_UNIMPLEMENTED

/datum/tgs_api/proc/InstanceName()
	return TGS_UNIMPLEMENTED

/datum/tgs_api/proc/TestMerges()
	return TGS_UNIMPLEMENTED

/datum/tgs_api/proc/EndProcess()
	return TGS_UNIMPLEMENTED

/datum/tgs_api/proc/Revision()
	return TGS_UNIMPLEMENTED

/datum/tgs_api/proc/ChatChannelInfo()
	return TGS_UNIMPLEMENTED

/datum/tgs_api/proc/ChatBroadcast(message, list/channels)
	return TGS_UNIMPLEMENTED

/datum/tgs_api/proc/ChatTargetedBroadcast(message, admin_only)
	return TGS_UNIMPLEMENTED

/datum/tgs_api/proc/ChatPrivateMessage(message, datum/tgs_chat_user/user)
	return TGS_UNIMPLEMENTED

/datum/tgs_api/proc/SecurityLevel()
	return TGS_UNIMPLEMENTED

repo_name: AnalWerewolf/sundown

license: mit


code:
/datum/tgs_version/New(raw_parameter)
	src.raw_parameter = raw_parameter
	deprefixed_parameter = replacetext(raw_parameter, "/tg/station 13 Server v", "")
	var/list/version_bits = splittext(deprefixed_parameter, ".")

	suite = text2num(version_bits[1])
	if(version_bits.len > 1)
		minor = text2num(version_bits[2])
		if(version_bits.len > 2)
			patch = text2num(version_bits[3])
			if(version_bits.len == 4)
				deprecated_patch = text2num(version_bits[4])

/datum/tgs_version/proc/Valid(allow_wildcards = FALSE)
	if(suite == null)
		return FALSE
	if(allow_wildcards)
		return TRUE
	return !Wildcard()

/datum/tgs_version/Wildcard()
	return minor == null || patch == null

/datum/tgs_version/Equals(datum/tgs_version/other_version)
	if(!istype(other_version))
		return FALSE

	return suite == other_version.suite && minor == other_version.minor && patch == other_version.patch && deprecated_patch == other_version.deprecated_patch


code:
#include "core\_definitions.dm"
#include "core\core.dm"
#include "core\datum.dm"
#include "core\tgs_version.dm"

#ifdef TGS_V3_API
#include "v3210\api.dm"
#include "v3210\commands.dm"
#endif

#include "v4\api.dm"
#include "v4\commands.dm"

#include "v5\_defines.dm"
#include "v5\api.dm"
#include "v5\commands.dm"
#include "v5\undefs.dm"


code:
# DMAPI V3

This DMAPI implements bridge using file output which TGS monitors for.

- [api.dm](./api.dm) contains the bulk of the API code.
- [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s.


code:
#define REBOOT_MODE_NORMAL 0
#define REBOOT_MODE_HARD 1
#define REBOOT_MODE_SHUTDOWN 2

#define SERVICE_WORLD_PARAM "server_service"
#define SERVICE_INSTANCE_PARAM "server_instance"
#define SERVICE_PR_TEST_JSON "prtestjob.json"
#define SERVICE_INTERFACE_DLL "TGDreamDaemonBridge.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_LIST_CUSTOM "list_custom_commands"
#define SERVICE_CMD_API_COMPATIBLE "api_compat"
#define SERVICE_CMD_PLAYER_COUNT "client_count"

#define SERVICE_CMD_PARAM_KEY "serviceCommsKey"
#define SERVICE_CMD_PARAM_COMMAND "command"
#define SERVICE_CMD_PARAM_SENDER "sender"
#define SERVICE_CMD_PARAM_CUSTOM "custom"

#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"

#define SERVICE_RETURN_SUCCESS "SUCCESS"

/datum/tgs_api/v3210
	var/reboot_mode = REBOOT_MODE_NORMAL
	var/comms_key
	var/instance_name
	var/originmastercommit
	var/commit
	var/list/cached_custom_tgs_chat_commands
	var/warned_revison = FALSE
	var/warned_custom_commands = FALSE

/datum/tgs_api/v3210/ApiVersion()
	return new /datum/tgs_version("3.2.1.3")

/datum/tgs_api/v3210/proc/trim_left(text)
	for (var/i = 1 to length(text))
		if (text2ascii(text, i) > 32)
			return copytext(text, i)
	return ""

/datum/tgs_api/v3210/proc/trim_right(text)
	for (var/i = length(text), i > 0, i--)
		if (text2ascii(text, i) > 32)
			return copytext(text, 1, i + 1)
	return ""

/datum/tgs_api/v3210/proc/file2list(filename)
	return splittext(trim_left(trim_right(file2text(filename))), "\n")

/datum/tgs_api/v3210/OnWorldNew(minimum_required_security_level)
	. = FALSE

	comms_key = world.params[SERVICE_WORLD_PARAM]
	instance_name = world.params[SERVICE_INSTANCE_PARAM]
	if(!instance_name)
		instance_name = "TG Station Server" //maybe just upgraded

	var/list/logs = file2list(".git/logs/HEAD")
	if(logs.len)
		logs = splittext(logs[logs.len - 1], " ")
		commit = logs[2]
	logs = file2list(".git/logs/refs/remotes/origin/master")
	if(logs.len)
		originmastercommit = splittext(logs[logs.len - 1], " ")[2]

	if(world.system_type != MS_WINDOWS)
		TGS_ERROR_LOG("This API version is only supported on Windows. Not running on Windows. Aborting initialization!")
		return
	ListServiceCustomCommands(TRUE)
	var/datum/tgs_version/api_version = ApiVersion()
	ExportService("[SERVICE_REQUEST_API_VERSION] [api_version.deprefixed_parameter]", TRUE)
	return TRUE

//nothing to do for v3
/datum/tgs_api/v3210/OnInitializationComplete()
	return

/datum/tgs_api/v3210/InstanceName()
	return world.params[SERVICE_INSTANCE_PARAM]

/datum/tgs_api/v3210/proc/ExportService(command, skip_compat_check = FALSE)
	. = FALSE
	if(skip_compat_check && !fexists(SERVICE_INTERFACE_DLL))
		TGS_ERROR_LOG("Service parameter present but no interface DLL detected. This is symptomatic of running a service less than version 3.1! Please upgrade.")
		return
	call(SERVICE_INTERFACE_DLL, SERVICE_INTERFACE_FUNCTION)(instance_name, command) //trust no retval
	return TRUE

/datum/tgs_api/v3210/OnTopic(T)
	var/list/params = params2list(T)
	var/their_sCK = params[SERVICE_CMD_PARAM_KEY]
	if(!their_sCK)
		return FALSE //continue world/Topic

	if(their_sCK != comms_key)
		return "Invalid comms key!";

	var/command = params[SERVICE_CMD_PARAM_COMMAND]
	if(!command)
		return "No command!"

	switch(command)
		if(SERVICE_CMD_API_COMPATIBLE)
			return SERVICE_RETURN_SUCCESS
		if(SERVICE_CMD_HARD_REBOOT)
			if(reboot_mode != REBOOT_MODE_HARD)
				reboot_mode = REBOOT_MODE_HARD
				TGS_INFO_LOG("Hard reboot requested by service")
				TGS_NOTIFY_ADMINS("The world will hard reboot at the end of the game. Requested by TGS.")
		if(SERVICE_CMD_GRACEFUL_SHUTDOWN)
			if(reboot_mode != REBOOT_MODE_SHUTDOWN)
				reboot_mode = REBOOT_MODE_SHUTDOWN
				TGS_INFO_LOG("Shutdown requested by service")
				TGS_NOTIFY_ADMINS("The world will shutdown at the end of the game. Requested by TGS.")
		if(SERVICE_CMD_WORLD_ANNOUNCE)
			var/msg = params["message"]
			if(!istext(msg) || !msg)
				return "No message set!"
			TGS_WORLD_ANNOUNCE(msg)
			return SERVICE_RETURN_SUCCESS
		if(SERVICE_CMD_PLAYER_COUNT)
			return "[TGS_CLIENT_COUNT]"
		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 : SERVICE_RETURN_SUCCESS
	return "Unknown command: [command]"

/datum/tgs_api/v3210/OnReboot()
	switch(reboot_mode)
		if(REBOOT_MODE_HARD)
			TGS_WORLD_ANNOUNCE("Hard reboot triggered, you will automatically reconnect...")
			EndProcess()
		if(REBOOT_MODE_SHUTDOWN)
			TGS_WORLD_ANNOUNCE("The server is shutting down...")
			EndProcess()
		else
			ExportService(SERVICE_REQUEST_WORLD_REBOOT) //just let em know

/datum/tgs_api/v3210/TestMerges()
	//do the best we can here as the datum can't be completed using the v3 api
	. = list()
	if(!fexists(SERVICE_PR_TEST_JSON))
		return
	var/list/json = json_decode(file2text(SERVICE_PR_TEST_JSON))
	if(!json)
		return
	for(var/I in json)
		var/datum/tgs_revision_information/test_merge/tm = new
		tm.number = text2num(I)
		var/list/entry = json[I]
		tm.head_commit = entry["commit"]
		tm.author = entry["author"]
		tm.title = entry["title"]
		. += tm

/datum/tgs_api/v3210/Revision()
	if(!warned_revison)
		var/datum/tgs_version/api_version = ApiVersion()
		TGS_ERROR_LOG("Use of TgsRevision on [api_version.deprefixed_parameter] origin_commit only points to master!")
		warned_revison = TRUE
	var/datum/tgs_revision_information/ri = new
	ri.commit = commit
	ri.origin_commit = originmastercommit
	return ri

/datum/tgs_api/v3210/EndProcess()
	sleep(world.tick_lag) //flush the buffers
	ExportService(SERVICE_REQUEST_KILL_PROCESS)

/datum/tgs_api/v3210/ChatChannelInfo()
	return list() // :omegalul:

/datum/tgs_api/v3210/ChatBroadcast(message, list/channels)
	if(channels)
		return TGS_UNIMPLEMENTED
	ChatTargetedBroadcast(message, TRUE)
	ChatTargetedBroadcast(message, FALSE)

/datum/tgs_api/v3210/ChatTargetedBroadcast(message, admin_only)
	ExportService("[admin_only ? SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE : SERVICE_REQUEST_IRC_BROADCAST] [message]")

/datum/tgs_api/v3210/ChatPrivateMessage(message, datum/tgs_chat_user/user)
	return TGS_UNIMPLEMENTED

/datum/tgs_api/v3210/SecurityLevel()
	return TGS_SECURITY_TRUSTED

#undef REBOOT_MODE_NORMAL
#undef REBOOT_MODE_HARD
#undef REBOOT_MODE_SHUTDOWN

#undef SERVICE_WORLD_PARAM
#undef SERVICE_INSTANCE_PARAM
#undef SERVICE_PR_TEST_JSON
#undef SERVICE_INTERFACE_DLL
#undef SERVICE_INTERFACE_FUNCTION

#undef SERVICE_CMD_HARD_REBOOT
#undef SERVICE_CMD_GRACEFUL_SHUTDOWN
#undef SERVICE_CMD_WORLD_ANNOUNCE
#undef SERVICE_CMD_LIST_CUSTOM
#undef SERVICE_CMD_API_COMPATIBLE
#undef SERVICE_CMD_PLAYER_COUNT

#undef SERVICE_CMD_PARAM_KEY
#undef SERVICE_CMD_PARAM_COMMAND
#undef SERVICE_CMD_PARAM_SENDER
#undef SERVICE_CMD_PARAM_CUSTOM

#undef SERVICE_REQUEST_KILL_PROCESS
#undef SERVICE_REQUEST_IRC_BROADCAST
#undef SERVICE_REQUEST_IRC_ADMIN_CHANNEL_MESSAGE
#undef SERVICE_REQUEST_WORLD_REBOOT
#undef SERVICE_REQUEST_API_VERSION

#undef SERVICE_RETURN_SUCCESS


code:
#define SERVICE_JSON_PARAM_HELPTEXT "help_text"
#define SERVICE_JSON_PARAM_ADMINONLY "admin_only"
#define SERVICE_JSON_PARAM_REQUIREDPARAMETERS "required_parameters"

/datum/tgs_api/v3210/proc/ListServiceCustomCommands(warnings_only)
	if(!warnings_only)
		. = list()
	var/list/command_name_types = list()
	var/list/warned_command_names = warnings_only ? list() : null
	var/warned_about_the_dangers_of_robutussin = !warnings_only
	for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command)
		if(!warned_about_the_dangers_of_robutussin)
			TGS_ERROR_LOG("Custom chat commands in [ApiVersion()] lacks the /datum/tgs_chat_user/sender.channel field!")
			warned_about_the_dangers_of_robutussin = TRUE
		var/datum/tgs_chat_command/stc = I
		var/command_name = initial(stc.name)
		if(!command_name || findtext(command_name, " ") || findtext(command_name, "'") || findtext(command_name, "\""))
			if(warnings_only && !warned_command_names[command_name])
				TGS_ERROR_LOG("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)
				TGS_ERROR_LOG("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 = 0)

/datum/tgs_api/v3210/proc/HandleServiceCustomCommand(command, sender, params)
	if(!cached_custom_tgs_chat_commands)
		cached_custom_tgs_chat_commands = list()
		for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command)
			var/datum/tgs_chat_command/stc = I
			cached_custom_tgs_chat_commands[lowertext(initial(stc.name))] = stc

	var/command_type = cached_custom_tgs_chat_commands[command]
	if(!command_type)
		return FALSE
	var/datum/tgs_chat_command/stc = new command_type
	var/datum/tgs_chat_user/user = new
	user.friendly_name = sender

	// Discord hack, fix the mention if it's only numbers (fuck you IRC trolls)
	var/regex/discord_id_regex = regex(@"^[0-9]+$")
	if(findtext(sender, discord_id_regex))
		sender = "<@[sender]>"

	user.mention = sender
	return stc.Run(user, params) || TRUE


code:
# DMAPI V4

This DMAPI implements bridge requests using file output which TGS monitors for. It has a safe mode restriction.

- [api.dm](./api.dm) contains the bulk of the API code.
- [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s.


code:
#define TGS4_PARAM_INFO_JSON "tgs_json"

#define TGS4_INTEROP_ACCESS_IDENTIFIER "tgs_tok"

#define TGS4_RESPONSE_SUCCESS "tgs_succ"

#define TGS4_TOPIC_CHANGE_PORT "tgs_port"
#define TGS4_TOPIC_CHANGE_REBOOT_MODE "tgs_rmode"
#define TGS4_TOPIC_CHAT_COMMAND "tgs_chat_comm"
#define TGS4_TOPIC_EVENT "tgs_event"
#define TGS4_TOPIC_INTEROP_RESPONSE "tgs_interop"

#define TGS4_COMM_NEW_PORT "tgs_new_port"
#define TGS4_COMM_VALIDATE "tgs_validate"
#define TGS4_COMM_SERVER_PRIMED "tgs_prime"
#define TGS4_COMM_WORLD_REBOOT "tgs_reboot"
#define TGS4_COMM_END_PROCESS "tgs_kill"
#define TGS4_COMM_CHAT "tgs_chat_send"

#define TGS4_PARAMETER_COMMAND "tgs_com"
#define TGS4_PARAMETER_DATA "tgs_data"

#define TGS4_PORT_CRITFAIL_MESSAGE " Must exit to let watchdog reboot..."

#define EXPORT_TIMEOUT_DS 200

/datum/tgs_api/v4
	var/access_identifier
	var/instance_name
	var/json_path
	var/chat_channels_json_path
	var/chat_commands_json_path
	var/server_commands_json_path
	var/reboot_mode = TGS_REBOOT_MODE_NORMAL
	var/security_level

	var/requesting_new_port = FALSE

	var/list/intercepted_message_queue

	var/list/custom_commands

	var/list/cached_test_merges
	var/datum/tgs_revision_information/cached_revision

	var/export_lock = FALSE
	var/list/last_interop_response

/datum/tgs_api/v4/ApiVersion()
	return new /datum/tgs_version("4.0.0.0")

/datum/tgs_api/v4/OnWorldNew(minimum_required_security_level)
	if(minimum_required_security_level == TGS_SECURITY_ULTRASAFE)
		TGS_WARNING_LOG("V4 DMAPI requires safe security!")
		minimum_required_security_level = TGS_SECURITY_SAFE

	json_path = world.params[TGS4_PARAM_INFO_JSON]
	if(!json_path)
		TGS_ERROR_LOG("Missing [TGS4_PARAM_INFO_JSON] world parameter!")
		return
	var/json_file = file2text(json_path)
	if(!json_file)
		TGS_ERROR_LOG("Missing specified json file: [json_path]")
		return
	var/cached_json = json_decode(json_file)
	if(!cached_json)
		TGS_ERROR_LOG("Failed to decode info json: [json_file]")
		return

	access_identifier = cached_json["accessIdentifier"]
	server_commands_json_path = cached_json["serverCommandsJson"]

	if(cached_json["apiValidateOnly"])
		TGS_INFO_LOG("Validating API and exiting...")
		Export(TGS4_COMM_VALIDATE, list(TGS4_PARAMETER_DATA = "[minimum_required_security_level]"))
		del(world)

	security_level = cached_json["securityLevel"]
	chat_channels_json_path = cached_json["chatChannelsJson"]
	chat_commands_json_path = cached_json["chatCommandsJson"]
	instance_name = cached_json["instanceName"]

	ListCustomCommands()

	var/list/revisionData = cached_json["revision"]
	if(revisionData)
		cached_revision = new
		cached_revision.commit = revisionData["commitSha"]
		cached_revision.origin_commit = revisionData["originCommitSha"]

	cached_test_merges = list()
	var/list/json = cached_json["testMerges"]
	for(var/entry in json)
		var/datum/tgs_revision_information/test_merge/tm = new
		tm.timestamp = text2num(entry["timeMerged"])

		var/list/revInfo = entry["revision"]
		if(revInfo)
			tm.commit = revInfo["commitSha"]
			tm.origin_commit = revInfo["originCommitSha"]

		tm.title = entry["titleAtMerge"]
		tm.body = entry["bodyAtMerge"]
		tm.url = entry["url"]
		tm.author = entry["author"]
		tm.number = entry["number"]
		tm.head_commit = entry["pullRequestRevision"]
		tm.comment = entry["comment"]

		cached_test_merges += tm

	return TRUE

/datum/tgs_api/v4/OnInitializationComplete()
	Export(TGS4_COMM_SERVER_PRIMED)

/datum/tgs_api/v4/OnTopic(T)
	var/list/params = params2list(T)
	var/their_sCK = params[TGS4_INTEROP_ACCESS_IDENTIFIER]
	if(!their_sCK)
		return FALSE //continue world/Topic

	if(their_sCK != access_identifier)
		return "Invalid comms key!";

	var/command = params[TGS4_PARAMETER_COMMAND]
	if(!command)
		return "No command!"

	. = TGS4_RESPONSE_SUCCESS

	switch(command)
		if(TGS4_TOPIC_CHAT_COMMAND)
			var/result = HandleCustomCommand(params[TGS4_PARAMETER_DATA])
			if(result == null)
				result = "Error running chat command!"
			return result
		if(TGS4_TOPIC_EVENT)
			intercepted_message_queue = list()
			var/list/event_notification = json_decode(params[TGS4_PARAMETER_DATA])
			var/list/event_parameters = event_notification["Parameters"]

			var/list/event_call = list(event_notification["Type"])
			if(event_parameters)
				event_call += event_parameters

			if(event_handler != null)
				event_handler.HandleEvent(arglist(event_call))

			. = json_encode(intercepted_message_queue)
			intercepted_message_queue = null
			return
		if(TGS4_TOPIC_INTEROP_RESPONSE)
			last_interop_response = json_decode(params[TGS4_PARAMETER_DATA])
			return
		if(TGS4_TOPIC_CHANGE_PORT)
			var/new_port = text2num(params[TGS4_PARAMETER_DATA])
			if (!(new_port > 0))
				return "Invalid port: [new_port]"

			//the topic still completes, miraculously
			//I honestly didn't believe byond could do it
			if(event_handler != null)
				event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port)
			if(!world.OpenPort(new_port))
				return "Port change failed!"
			return
		if(TGS4_TOPIC_CHANGE_REBOOT_MODE)
			var/new_reboot_mode = text2num(params[TGS4_PARAMETER_DATA])
			if(event_handler != null)
				event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode)
			reboot_mode = new_reboot_mode
			return

	return "Unknown command: [command]"

/datum/tgs_api/v4/proc/Export(command, list/data, override_requesting_new_port = FALSE)
	if(!data)
		data = list()
	data[TGS4_PARAMETER_COMMAND] = command
	var/json = json_encode(data)

	while(requesting_new_port && !override_requesting_new_port)
		sleep(1)

	//we need some port open at this point to facilitate return communication
	if(!world.port)
		requesting_new_port = TRUE
		if(!world.OpenPort(0)) //open any port
			TGS_ERROR_LOG("Unable to open random port to retrieve new port![TGS4_PORT_CRITFAIL_MESSAGE]")
			del(world)

		//request a new port
		export_lock = FALSE
		var/list/new_port_json = Export(TGS4_COMM_NEW_PORT, list(TGS4_PARAMETER_DATA = "[world.port]"), TRUE) //stringify this on purpose

		if(!new_port_json)
			TGS_ERROR_LOG("No new port response from server![TGS4_PORT_CRITFAIL_MESSAGE]")
			del(world)

		var/new_port = new_port_json[TGS4_PARAMETER_DATA]
		if(!isnum(new_port) || new_port <= 0)
			TGS_ERROR_LOG("Malformed new port json ([json_encode(new_port_json)])![TGS4_PORT_CRITFAIL_MESSAGE]")
			del(world)

		if(new_port != world.port && !world.OpenPort(new_port))
			TGS_ERROR_LOG("Unable to open port [new_port]![TGS4_PORT_CRITFAIL_MESSAGE]")
			del(world)
		requesting_new_port = FALSE

	while(export_lock)
		sleep(1)
	export_lock = TRUE

	last_interop_response = null
	fdel(server_commands_json_path)
	text2file(json, server_commands_json_path)

	for(var/I = 0; I < EXPORT_TIMEOUT_DS && !last_interop_response; ++I)
		sleep(1)

	if(!last_interop_response)
		TGS_ERROR_LOG("Failed to get export result for: [json]")
	else
		. = last_interop_response

	export_lock = FALSE

/datum/tgs_api/v4/OnReboot()
	var/list/result = Export(TGS4_COMM_WORLD_REBOOT)
	if(!result)
		return

	//okay so the standard TGS4 proceedure is: right before rebooting change the port to whatever was sent to us in the above json's data parameter

	var/port = result[TGS4_PARAMETER_DATA]
	if(!isnum(port))
		return //this is valid, server may just want use to reboot

	if(port == 0)
		//to byond 0 means any port and "none" means close vOv
		port = "none"

	if(!world.OpenPort(port))
		TGS_ERROR_LOG("Unable to set port to [port]!")

/datum/tgs_api/v4/InstanceName()
	return instance_name

/datum/tgs_api/v4/TestMerges()
	return cached_test_merges.Copy()

/datum/tgs_api/v4/EndProcess()
	Export(TGS4_COMM_END_PROCESS)

/datum/tgs_api/v4/Revision()
	return cached_revision

/datum/tgs_api/v4/ChatBroadcast(message, list/channels)
	var/list/ids
	if(length(channels))
		ids = list()
		for(var/I in channels)
			var/datum/tgs_chat_channel/channel = I
			ids += channel.id
	message = list("message" = message, "channelIds" = ids)
	if(intercepted_message_queue)
		intercepted_message_queue += list(message)
	else
		Export(TGS4_COMM_CHAT, message)

/datum/tgs_api/v4/ChatTargetedBroadcast(message, admin_only)
	var/list/channels = list()
	for(var/I in ChatChannelInfo())
		var/datum/tgs_chat_channel/channel = I
		if (!channel.is_private_channel && ((channel.is_admin_channel && admin_only) || (!channel.is_admin_channel && !admin_only)))
			channels += channel.id
	message = list("message" = message, "channelIds" = channels)
	if(intercepted_message_queue)
		intercepted_message_queue += list(message)
	else
		Export(TGS4_COMM_CHAT, message)

/datum/tgs_api/v4/ChatPrivateMessage(message, datum/tgs_chat_user/user)
	message = list("message" = message, "channelIds" = list(user.channel.id))
	if(intercepted_message_queue)
		intercepted_message_queue += list(message)
	else
		Export(TGS4_COMM_CHAT, message)

/datum/tgs_api/v4/ChatChannelInfo()
	. = list()
	//no caching cause tgs may change this
	var/list/json = json_decode(file2text(chat_channels_json_path))
	for(var/I in json)
		. += DecodeChannel(I)

/datum/tgs_api/v4/proc/DecodeChannel(channel_json)
	var/datum/tgs_chat_channel/channel = new
	channel.id = channel_json["id"]
	channel.friendly_name = channel_json["friendlyName"]
	channel.connection_name = channel_json["connectionName"]
	channel.is_admin_channel = channel_json["isAdminChannel"]
	channel.is_private_channel = channel_json["isPrivateChannel"]
	channel.custom_tag = channel_json["tag"]
	return channel

/datum/tgs_api/v4/SecurityLevel()
	return security_level


code:
/datum/tgs_api/v4/proc/ListCustomCommands()
	var/results = list()
	custom_commands = list()
	for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command)
		var/datum/tgs_chat_command/stc = new I
		var/command_name = stc.name
		if(!command_name || findtext(command_name, " ") || findtext(command_name, "'") || findtext(command_name, "\""))
			TGS_ERROR_LOG("Custom command [command_name] ([I]) can't be used as it is empty or contains illegal characters!")
			continue

		if(results[command_name])
			var/datum/other = custom_commands[command_name]
			TGS_ERROR_LOG("Custom commands [other.type] and [I] have the same name (\"[command_name]\"), only [other.type] will be available!")
			continue
		results += list(list("name" = command_name, "help_text" = stc.help_text, "admin_only" = stc.admin_only))
		custom_commands[command_name] = stc

	var/commands_file = chat_commands_json_path
	if(!commands_file)
		return
	text2file(json_encode(results), commands_file)

/datum/tgs_api/v4/proc/HandleCustomCommand(command_json)
	var/list/data = json_decode(command_json)
	var/command = data["command"]
	var/user = data["user"]
	var/params = data["params"]

	var/datum/tgs_chat_user/u = new
	u.id = user["id"]
	u.friendly_name = user["friendlyName"]
	u.mention = user["mention"]
	u.channel = DecodeChannel(user["channel"])

	var/datum/tgs_chat_command/sc = custom_commands[command]
	if(sc)
		var/result = sc.Run(u, params)
		if(result == null)
			result = ""
		return result
	return "Unknown command: [command]!"


code:
# DMAPI V5

This DMAPI implements bridge requests using HTTP GET requests to TGS. It has no security restrictions.

- [_defines.dm](./_defines.dm) contains constant definitions.
- [api.dm](./api.dm) contains the bulk of the API code.
- [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s.
- [undefs.dm](./undefs.dm) Undoes the work of `_defines.dm`.


code:
#define DMAPI5_PARAM_SERVER_PORT "tgs_port"
#define DMAPI5_PARAM_ACCESS_IDENTIFIER "tgs_key"

#define DMAPI5_BRIDGE_DATA "data"
#define DMAPI5_TOPIC_DATA "tgs_data"

#define DMAPI5_BRIDGE_COMMAND_PORT_UPDATE 0
#define DMAPI5_BRIDGE_COMMAND_STARTUP 1
#define DMAPI5_BRIDGE_COMMAND_PRIME 2
#define DMAPI5_BRIDGE_COMMAND_REBOOT 3
#define DMAPI5_BRIDGE_COMMAND_KILL 4
#define DMAPI5_BRIDGE_COMMAND_CHAT_SEND 5

#define DMAPI5_PARAMETER_ACCESS_IDENTIFIER "accessIdentifier"
#define DMAPI5_PARAMETER_CUSTOM_COMMANDS "customCommands"

#define DMAPI5_RESPONSE_ERROR_MESSAGE "errorMessage"

#define DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE "commandType"
#define DMAPI5_BRIDGE_PARAMETER_CURRENT_PORT "currentPort"
#define DMAPI5_BRIDGE_PARAMETER_VERSION "version"
#define DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE "chatMessage"
#define DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL "minimumSecurityLevel"

#define DMAPI5_BRIDGE_RESPONSE_NEW_PORT "newPort"
#define DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION "runtimeInformation"

#define DMAPI5_CHAT_MESSAGE_TEXT "text"
#define DMAPI5_CHAT_MESSAGE_CHANNEL_IDS "channelIds"

#define DMAPI5_RUNTIME_INFORMATION_ACCESS_IDENTIFIER "accessIdentifier"
#define DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION "serverVersion"
#define DMAPI5_RUNTIME_INFORMATION_SERVER_PORT "serverPort"
#define DMAPI5_RUNTIME_INFORMATION_API_VALIDATE_ONLY "apiValidateOnly"
#define DMAPI5_RUNTIME_INFORMATION_INSTANCE_NAME "instanceName"
#define DMAPI5_RUNTIME_INFORMATION_REVISION "revision"
#define DMAPI5_RUNTIME_INFORMATION_TEST_MERGES "testMerges"
#define DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL "securityLevel"

#define DMAPI5_CHAT_UPDATE_CHANNELS "channels"

#define DMAPI5_TEST_MERGE_TIME_MERGED "timeMerged"
#define DMAPI5_TEST_MERGE_REVISION "revision"
#define DMAPI5_TEST_MERGE_TITLE_AT_MERGE "titleAtMerge"
#define DMAPI5_TEST_MERGE_BODY_AT_MERGE "bodyAtMerge"
#define DMAPI5_TEST_MERGE_URL "url"
#define DMAPI5_TEST_MERGE_AUTHOR "author"
#define DMAPI5_TEST_MERGE_NUMBER "number"
#define DMAPI5_TEST_MERGE_PULL_REQUEST_REVISION "pullRequestRevision"
#define DMAPI5_TEST_MERGE_COMMENT "comment"

#define DMAPI5_CHAT_COMMAND_NAME "name"
#define DMAPI5_CHAT_COMMAND_PARAMS "params"
#define DMAPI5_CHAT_COMMAND_USER "user"

#define DMAPI5_EVENT_NOTIFICATION_TYPE "type"
#define DMAPI5_EVENT_NOTIFICATION_PARAMETERS "parameters"

#define DMAPI5_TOPIC_COMMAND_CHAT_COMMAND 0
#define DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION 1
#define DMAPI5_TOPIC_COMMAND_CHANGE_PORT 2
#define DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE 3
#define DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED 4
#define DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE 5
#define DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE 6
#define DMAPI5_TOPIC_COMMAND_HEARTBEAT 7
#define DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH 8

#define DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE "commandType"
#define DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND "chatCommand"
#define DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION "eventNotification"
#define DMAPI5_TOPIC_PARAMETER_NEW_PORT "newPort"
#define DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE "newRebootState"
#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_RESPONSE_COMMAND_RESPONSE_MESSAGE "commandResponseMessage"
#define DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES "chatResponses"

#define DMAPI5_REVISION_INFORMATION_COMMIT_SHA "commitSha"
#define DMAPI5_REVISION_INFORMATION_TIMESTAMP "timestamp"
#define DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA "originCommitSha"

#define DMAPI5_CHAT_USER_ID "id"
#define DMAPI5_CHAT_USER_FRIENDLY_NAME "friendlyName"
#define DMAPI5_CHAT_USER_MENTION "mention"
#define DMAPI5_CHAT_USER_CHANNEL "channel"

#define DMAPI5_CHAT_CHANNEL_ID "id"
#define DMAPI5_CHAT_CHANNEL_FRIENDLY_NAME "friendlyName"
#define DMAPI5_CHAT_CHANNEL_CONNECTION_NAME "connectionName"
#define DMAPI5_CHAT_CHANNEL_IS_ADMIN_CHANNEL "isAdminChannel"
#define DMAPI5_CHAT_CHANNEL_IS_PRIVATE_CHANNEL "isPrivateChannel"
#define DMAPI5_CHAT_CHANNEL_TAG "tag"

#define DMAPI5_CUSTOM_CHAT_COMMAND_NAME "name"
#define DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT "helpText"
#define DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY "adminOnly"


code:
/datum/tgs_api/v5
	var/server_port
	var/access_identifier

	var/instance_name
	var/security_level

	var/reboot_mode = TGS_REBOOT_MODE_NORMAL

	var/list/intercepted_message_queue

	var/list/custom_commands

	var/list/test_merges
	var/datum/tgs_revision_information/revision
	var/list/chat_channels

	var/initialized = FALSE

/datum/tgs_api/v5/ApiVersion()
	return new /datum/tgs_version(
		#include "interop_version.dm"
	)

/datum/tgs_api/v5/OnWorldNew(minimum_required_security_level)
	server_port = world.params[DMAPI5_PARAM_SERVER_PORT]
	access_identifier = world.params[DMAPI5_PARAM_ACCESS_IDENTIFIER]

	var/datum/tgs_version/api_version = ApiVersion()
	version = null
	var/list/bridge_response = Bridge(DMAPI5_BRIDGE_COMMAND_STARTUP, list(DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL = minimum_required_security_level, DMAPI5_BRIDGE_PARAMETER_VERSION = api_version.raw_parameter, DMAPI5_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands()))
	if(!istype(bridge_response))
		TGS_ERROR_LOG("Failed initial bridge request!")
		return FALSE

	var/list/runtime_information = bridge_response[DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION]
	if(!istype(runtime_information))
		TGS_ERROR_LOG("Failed to decode runtime information from bridge response: [json_encode(bridge_response)]!")
		return FALSE

	if(runtime_information[DMAPI5_RUNTIME_INFORMATION_API_VALIDATE_ONLY])
		TGS_INFO_LOG("DMAPI validation, exiting...")
		del(world)

	version = new /datum/tgs_version(runtime_information[DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION])
	security_level = runtime_information[DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL]
	instance_name = runtime_information[DMAPI5_RUNTIME_INFORMATION_INSTANCE_NAME]

	var/list/revisionData = runtime_information[DMAPI5_RUNTIME_INFORMATION_REVISION]
	if(istype(revisionData))
		revision = new
		revision.commit = revisionData[DMAPI5_REVISION_INFORMATION_COMMIT_SHA]
		revision.timestamp = revisionData[DMAPI5_REVISION_INFORMATION_TIMESTAMP]
		revision.origin_commit = revisionData[DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA]
	else
		TGS_ERROR_LOG("Failed to decode [DMAPI5_RUNTIME_INFORMATION_REVISION] from runtime information!")

	test_merges = list()
	var/list/test_merge_json = runtime_information[DMAPI5_RUNTIME_INFORMATION_TEST_MERGES]
	if(istype(test_merge_json))
		for(var/entry in test_merge_json)
			var/datum/tgs_revision_information/test_merge/tm = new
			tm.number = entry[DMAPI5_TEST_MERGE_NUMBER]

			var/list/revInfo = entry[DMAPI5_TEST_MERGE_REVISION]
			if(revInfo)
				tm.commit = revisionData[DMAPI5_REVISION_INFORMATION_COMMIT_SHA]
				tm.origin_commit = revisionData[DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA]
				tm.timestamp = entry[DMAPI5_REVISION_INFORMATION_TIMESTAMP]
			else
				TGS_WARNING_LOG("Failed to decode [DMAPI5_TEST_MERGE_REVISION] from test merge #[tm.number]!")

			if(!tm.timestamp)
				tm.timestamp = entry[DMAPI5_TEST_MERGE_TIME_MERGED]

			tm.title = entry[DMAPI5_TEST_MERGE_TITLE_AT_MERGE]
			tm.body = entry[DMAPI5_TEST_MERGE_BODY_AT_MERGE]
			tm.url = entry[DMAPI5_TEST_MERGE_URL]
			tm.author = entry[DMAPI5_TEST_MERGE_AUTHOR]
			tm.head_commit = entry[DMAPI5_TEST_MERGE_PULL_REQUEST_REVISION]
			tm.comment = entry[DMAPI5_TEST_MERGE_COMMENT]

			test_merges += tm
	else
		TGS_WARNING_LOG("Failed to decode [DMAPI5_RUNTIME_INFORMATION_TEST_MERGES] from runtime information!")

	chat_channels = list()
	DecodeChannels(runtime_information)

	initialized = TRUE
	return TRUE

/datum/tgs_api/v5/proc/RequireInitialBridgeResponse()
	while(!version)
		sleep(1)

/datum/tgs_api/v5/OnInitializationComplete()
	Bridge(DMAPI5_BRIDGE_COMMAND_PRIME)

/datum/tgs_api/v5/proc/TopicResponse(error_message = null)
	var/list/response = list()
	response[DMAPI5_RESPONSE_ERROR_MESSAGE] = error_message

	return json_encode(response)

/datum/tgs_api/v5/OnTopic(T)
	var/list/params = params2list(T)
	var/json = params[DMAPI5_TOPIC_DATA]
	if(!json)
		return FALSE // continue to /world/Topic

	var/list/topic_parameters = json_decode(json)
	if(!topic_parameters)
		return TopicResponse("Invalid topic parameters json!");

	if(!initialized)
		TGS_WARNING_LOG("Missed topic due to not being initialized: [T]")
		return TRUE // too early to handle, but it's still our responsibility

	var/their_sCK = topic_parameters[DMAPI5_PARAMETER_ACCESS_IDENTIFIER]
	if(their_sCK != access_identifier)
		return TopicResponse("Failed to decode [DMAPI5_PARAMETER_ACCESS_IDENTIFIER] from: [json]!");

	var/command = topic_parameters[DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE]
	if(!isnum(command))
		return TopicResponse("Failed to decode [DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE] from: [json]!")

	switch(command)
		if(DMAPI5_TOPIC_COMMAND_CHAT_COMMAND)
			var/result = HandleCustomCommand(topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND])
			if(!result)
				result = TopicResponse("Error running chat command!")
			return result
		if(DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION)
			intercepted_message_queue = list()
			var/list/event_notification = topic_parameters[DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]
			if(!istype(event_notification))
				return TopicResponse("Invalid [DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]!")

			var/event_type = event_notification[DMAPI5_EVENT_NOTIFICATION_TYPE]
			if(!isnum(event_type))
				return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_TYPE]!")

			var/list/event_parameters = event_notification[DMAPI5_EVENT_NOTIFICATION_PARAMETERS]
			if(event_parameters && !istype(event_parameters))
				return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_PARAMETERS]!")

			var/list/event_call = list(event_type)
			if(event_parameters)
				event_call += event_parameters

			if(event_handler != null)
				event_handler.HandleEvent(arglist(event_call))

			var/list/response = list()
			response[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue
			intercepted_message_queue = null
			return json_encode(response)
		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]]")

			if(event_handler != null)
				event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port)

			//the topic still completes, miraculously
			//I honestly didn't believe byond could do it without exploding
			if(!world.OpenPort(new_port))
				return TopicResponse("Port change failed!")

			return TopicResponse()
		if(DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE)
			var/new_reboot_mode = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]
			if(!isnum(new_reboot_mode))
				return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]!")

			if(event_handler != null)
				event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode)

			reboot_mode = new_reboot_mode
			return TopicResponse()
		if(DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED)
			var/new_instance_name = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]
			if(!istext(new_instance_name))
				return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]!")

			if(event_handler != null)
				event_handler.HandleEvent(TGS_EVENT_INSTANCE_RENAMED, new_instance_name)

			instance_name = new_instance_name
			return TopicResponse()
		if(DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE)
			var/list/chat_update_json = topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]
			if(!istype(chat_update_json))
				return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]!")

			DecodeChannels(chat_update_json)
			return TopicResponse()
		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]]")

			server_port = new_port
			return TopicResponse()
		if(DMAPI5_TOPIC_COMMAND_HEARTBEAT)
			return TopicResponse()
		if(DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH)
			var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT]
			var/error_message = null
			if (new_port != null)
				if (!isnum(new_port) || !(new_port > 0))
					error_message = "Invalid [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]"
				else
					server_port = new_port

			var/new_version_string = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION]
			if (!istext(new_version_string))
				if(error_message != null)
					error_message += ", "
				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)
					event_handler.HandleEvent(TGS_EVENT_WATCHDOG_REATTACH, new_version)

				version = new_version

			return json_encode(list(DMAPI5_RESPONSE_ERROR_MESSAGE = error_message, DMAPI5_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands()))

	return TopicResponse("Unknown command: [command]")

/datum/tgs_api/v5/proc/Bridge(command, list/data)
	if(!data)
		data = list()

	data[DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE] = command
	data[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] = access_identifier

	var/json = json_encode(data)
	var/encoded_json = url_encode(json)

	// This is an infinite sleep until we get a response
	var/export_response = world.Export("http://127.0.0.1:[server_port]/Bridge?[DMAPI5_BRIDGE_DATA]=[encoded_json]")
	if(!export_response)
		TGS_ERROR_LOG("Failed export request: [json]")
		return

	var/response_json = file2text(export_response["CONTENT"])
	if(!response_json)
		TGS_ERROR_LOG("Failed export request, missing content!")
		return

	var/list/bridge_response = json_decode(response_json)
	if(!bridge_response)
		TGS_ERROR_LOG("Failed export request, bad json: [response_json]")
		return

	var/error = bridge_response[DMAPI5_RESPONSE_ERROR_MESSAGE]
	if(error)
		TGS_ERROR_LOG("Failed export request, bad request: [error]")
		return

	return bridge_response

/datum/tgs_api/v5/OnReboot()
	var/list/result = Bridge(DMAPI5_BRIDGE_COMMAND_REBOOT)
	if(!result)
		return

	//okay so the standard TGS4 proceedure is: right before rebooting change the port to whatever was sent to us in the above json's data parameter

	var/port = result[DMAPI5_BRIDGE_RESPONSE_NEW_PORT]
	if(!isnum(port))
		return //this is valid, server may just want use to reboot

	if(port == 0)
		//to byond 0 means any port and "none" means close vOv
		port = "none"

	if(!world.OpenPort(port))
		TGS_ERROR_LOG("Unable to set port to [port]!")

/datum/tgs_api/v5/InstanceName()
	RequireInitialBridgeResponse()
	return instance_name

/datum/tgs_api/v5/TestMerges()
	RequireInitialBridgeResponse()
	return test_merges.Copy()

/datum/tgs_api/v5/EndProcess()
	Bridge(DMAPI5_BRIDGE_COMMAND_KILL)

/datum/tgs_api/v5/Revision()
	RequireInitialBridgeResponse()
	return revision

/datum/tgs_api/v5/ChatBroadcast(message, list/channels)
	if(!length(channels))
		channels = ChatChannelInfo()

	var/list/ids = list()
	for(var/I in channels)
		var/datum/tgs_chat_channel/channel = I
		ids += channel.id

	message = list(DMAPI5_CHAT_MESSAGE_TEXT = message, DMAPI5_CHAT_MESSAGE_CHANNEL_IDS = ids)
	if(intercepted_message_queue)
		intercepted_message_queue += list(message)
	else
		Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = message))

/datum/tgs_api/v5/ChatTargetedBroadcast(message, admin_only)
	var/list/channels = list()
	for(var/I in ChatChannelInfo())
		var/datum/tgs_chat_channel/channel = I
		if (!channel.is_private_channel && ((channel.is_admin_channel && admin_only) || (!channel.is_admin_channel && !admin_only)))
			channels += channel.id
	message = list(DMAPI5_CHAT_MESSAGE_TEXT = message, DMAPI5_CHAT_MESSAGE_CHANNEL_IDS = channels)
	if(intercepted_message_queue)
		intercepted_message_queue += list(message)
	else
		Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = message))

/datum/tgs_api/v5/ChatPrivateMessage(message, datum/tgs_chat_user/user)
	message = list(DMAPI5_CHAT_MESSAGE_TEXT = message, DMAPI5_CHAT_MESSAGE_CHANNEL_IDS = list(user.channel.id))
	if(intercepted_message_queue)
		intercepted_message_queue += list(message)
	else
		Bridge(DMAPI5_BRIDGE_COMMAND_CHAT_SEND, list(DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE = message))

/datum/tgs_api/v5/ChatChannelInfo()
	RequireInitialBridgeResponse()
	return chat_channels.Copy()

/datum/tgs_api/v5/proc/DecodeChannels(chat_update_json)
	var/list/chat_channels_json = chat_update_json[DMAPI5_CHAT_UPDATE_CHANNELS]
	if(istype(chat_channels_json))
		chat_channels.Cut()
		for(var/channel_json in chat_channels_json)
			var/datum/tgs_chat_channel/channel = DecodeChannel(channel_json)
			if(channel)
				chat_channels += channel
	else
		TGS_WARNING_LOG("Failed to decode [DMAPI5_CHAT_UPDATE_CHANNELS] from channel update!")

/datum/tgs_api/v5/proc/DecodeChannel(channel_json)
	var/datum/tgs_chat_channel/channel = new
	channel.id = channel_json[DMAPI5_CHAT_CHANNEL_ID]
	channel.friendly_name = channel_json[DMAPI5_CHAT_CHANNEL_FRIENDLY_NAME]
	channel.connection_name = channel_json[DMAPI5_CHAT_CHANNEL_CONNECTION_NAME]
	channel.is_admin_channel = channel_json[DMAPI5_CHAT_CHANNEL_IS_ADMIN_CHANNEL]
	channel.is_private_channel = channel_json[DMAPI5_CHAT_CHANNEL_IS_PRIVATE_CHANNEL]
	channel.custom_tag = channel_json[DMAPI5_CHAT_CHANNEL_TAG]
	return channel

/datum/tgs_api/v5/SecurityLevel()
	RequireInitialBridgeResponse()
	return security_level


code:
/datum/tgs_api/v5/proc/ListCustomCommands()
	var/results = list()
	custom_commands = list()
	for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command)
		var/datum/tgs_chat_command/stc = new I
		var/command_name = stc.name
		if(!command_name || findtext(command_name, " ") || findtext(command_name, "'") || findtext(command_name, "\""))
			TGS_WARNING_LOG("Custom command [command_name] ([I]) can't be used as it is empty or contains illegal characters!")
			continue

		if(results[command_name])
			var/datum/other = custom_commands[command_name]
			TGS_WARNING_LOG("Custom commands [other.type] and [I] have the same name (\"[command_name]\"), only [other.type] will be available!")
			continue
		results += list(list(DMAPI5_CUSTOM_CHAT_COMMAND_NAME = command_name, DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT = stc.help_text, DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY = stc.admin_only))
		custom_commands[command_name] = stc

	return results

/datum/tgs_api/v5/proc/HandleCustomCommand(list/command_json)
	var/command = command_json[DMAPI5_CHAT_COMMAND_NAME]
	var/user = command_json[DMAPI5_CHAT_COMMAND_USER]
	var/params = command_json[DMAPI5_CHAT_COMMAND_PARAMS]

	var/datum/tgs_chat_user/u = new
	u.id = user[DMAPI5_CHAT_USER_ID]
	u.friendly_name = user[DMAPI5_CHAT_USER_FRIENDLY_NAME]
	u.mention = user[DMAPI5_CHAT_USER_MENTION]
	u.channel = DecodeChannel(user[DMAPI5_CHAT_USER_CHANNEL])

	var/datum/tgs_chat_command/sc = custom_commands[command]
	if(sc)
		var/text_response = sc.Run(u, params)
		var/list/topic_response = list()
		if(!istext(text_response))
			TGS_ERROR_LOG("Custom command [command] should return a string! Got: \"[text_response]\"")
			text_response = null
		topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE] = text_response
		return json_encode(topic_response)
	return TopicResponse("Unknown custom chat command: [command]!")


code:
#undef DMAPI5_PARAM_SERVER_PORT
#undef DMAPI5_PARAM_ACCESS_IDENTIFIER

#undef DMAPI5_BRIDGE_DATA
#undef DMAPI5_TOPIC_DATA

#undef DMAPI5_BRIDGE_COMMAND_PORT_UPDATE
#undef DMAPI5_BRIDGE_COMMAND_STARTUP
#undef DMAPI5_BRIDGE_COMMAND_PRIME
#undef DMAPI5_BRIDGE_COMMAND_REBOOT
#undef DMAPI5_BRIDGE_COMMAND_KILL
#undef DMAPI5_BRIDGE_COMMAND_CHAT_SEND

#undef DMAPI5_PARAMETER_ACCESS_IDENTIFIER
#undef DMAPI5_PARAMETER_CUSTOM_COMMANDS

#undef DMAPI5_RESPONSE_ERROR_MESSAGE

#undef DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE
#undef DMAPI5_BRIDGE_PARAMETER_CURRENT_PORT
#undef DMAPI5_BRIDGE_PARAMETER_VERSION
#undef DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE
#undef DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL

#undef DMAPI5_BRIDGE_RESPONSE_NEW_PORT
#undef DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION

#undef DMAPI5_CHAT_MESSAGE_TEXT
#undef DMAPI5_CHAT_MESSAGE_CHANNEL_IDS

#undef DMAPI5_RUNTIME_INFORMATION_ACCESS_IDENTIFIER
#undef DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION
#undef DMAPI5_RUNTIME_INFORMATION_SERVER_PORT
#undef DMAPI5_RUNTIME_INFORMATION_API_VALIDATE_ONLY
#undef DMAPI5_RUNTIME_INFORMATION_INSTANCE_NAME
#undef DMAPI5_RUNTIME_INFORMATION_REVISION
#undef DMAPI5_RUNTIME_INFORMATION_TEST_MERGES
#undef DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL

#undef DMAPI5_CHAT_UPDATE_CHANNELS

#undef DMAPI5_TEST_MERGE_TIME_MERGED
#undef DMAPI5_TEST_MERGE_REVISION
#undef DMAPI5_TEST_MERGE_TITLE_AT_MERGE
#undef DMAPI5_TEST_MERGE_BODY_AT_MERGE
#undef DMAPI5_TEST_MERGE_URL
#undef DMAPI5_TEST_MERGE_AUTHOR
#undef DMAPI5_TEST_MERGE_NUMBER
#undef DMAPI5_TEST_MERGE_PULL_REQUEST_REVISION
#undef DMAPI5_TEST_MERGE_COMMENT

#undef DMAPI5_CHAT_COMMAND_NAME
#undef DMAPI5_CHAT_COMMAND_PARAMS
#undef DMAPI5_CHAT_COMMAND_USER

#undef DMAPI5_EVENT_NOTIFICATION_TYPE
#undef DMAPI5_EVENT_NOTIFICATION_PARAMETERS

#undef DMAPI5_TOPIC_COMMAND_CHAT_COMMAND
#undef DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION
#undef DMAPI5_TOPIC_COMMAND_CHANGE_PORT
#undef DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE
#undef DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED
#undef DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE
#undef DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE
#undef DMAPI5_TOPIC_COMMAND_HEARTBEAT
#undef DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH

#undef DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE
#undef DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND
#undef DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION
#undef DMAPI5_TOPIC_PARAMETER_NEW_PORT
#undef DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE
#undef DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME
#undef DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE
#undef DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION

#undef DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE
#undef DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES

#undef DMAPI5_REVISION_INFORMATION_COMMIT_SHA
#undef DMAPI5_REVISION_INFORMATION_TIMESTAMP
#undef DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA

#undef DMAPI5_CHAT_USER_ID
#undef DMAPI5_CHAT_USER_FRIENDLY_NAME
#undef DMAPI5_CHAT_USER_MENTION
#undef DMAPI5_CHAT_USER_CHANNEL

#undef DMAPI5_CHAT_CHANNEL_ID
#undef DMAPI5_CHAT_CHANNEL_FRIENDLY_NAME
#undef DMAPI5_CHAT_CHANNEL_CONNECTION_NAME
#undef DMAPI5_CHAT_CHANNEL_IS_ADMIN_CHANNEL
#undef DMAPI5_CHAT_CHANNEL_IS_PRIVATE_CHANNEL
#undef DMAPI5_CHAT_CHANNEL_TAG

#undef DMAPI5_CUSTOM_CHAT_COMMAND_NAME
#undef DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT
#undef DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY


code:
/*!
 * External tgui definitions, such as src_object APIs.
 *
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * public
 *
 * Used to open and update UIs.
 * If this proc is not implemented properly, the UI will not update correctly.
 *
 * required user mob The mob who opened/is using the UI.
 * optional ui datum/tgui The UI to be updated, if it exists.
 */
/datum/proc/ui_interact(mob/user, datum/tgui/ui)
	return FALSE // Not implemented.

/**
 * public
 *
 * Data to be sent to the UI.
 * This must be implemented for a UI to work.
 *
 * required user mob The mob interacting with the UI.
 *
 * return list Data to be sent to the UI.
 */
/datum/proc/ui_data(mob/user)
	return list() // Not implemented.

/**
 * public
 *
 * Static Data to be sent to the UI.
 *
 * Static data differs from normal data in that it's large data that should be
 * sent infrequently. This is implemented optionally for heavy uis that would
 * be sending a lot of redundant data frequently. Gets squished into one
 * object on the frontend side, but the static part is cached.
 *
 * required user mob The mob interacting with the UI.
 *
 * return list Statuic Data to be sent to the UI.
 */
/datum/proc/ui_static_data(mob/user)
	return list()

/**
 * public
 *
 * Forces an update on static data. Should be done manually whenever something
 * happens to change static data.
 *
 * required user the mob currently interacting with the ui
 * optional ui ui to be updated
 */
/datum/proc/update_static_data(mob/user, datum/tgui/ui)
	if(!ui)
		ui = SStgui.get_open_ui(user, src)
	if(ui)
		ui.send_full_update()

/**
 * public
 *
 * Called on a UI when the UI receieves a href.
 * Think of this as Topic().
 *
 * required action string The action/button that has been invoked by the user.
 * required params list A list of parameters attached to the button.
 *
 * return bool If the user's input has been handled and the UI should update.
 */
/datum/proc/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
	SHOULD_CALL_PARENT(TRUE)
	SEND_SIGNAL(src, COMSIG_UI_ACT, usr, action)
	// If UI is not interactive or usr calling Topic is not the UI user, bail.
	if(!ui || ui.status != UI_INTERACTIVE)
		return TRUE

/**
 * public
 *
 * Called on an object when a tgui object is being created, allowing you to
 * push various assets to tgui, for examples spritesheets.
 *
 * return list List of asset datums or file paths.
 */
/datum/proc/ui_assets(mob/user)
	return list()

/**
 * private
 *
 * The UI's host object (usually src_object).
 * This allows modules/datums to have the UI attached to them,
 * and be a part of another object.
 */
/datum/proc/ui_host(mob/user)
	return src // Default src.

/**
 * private
 *
 * The UI's state controller to be used for created uis
 * This is a proc over a var for memory reasons
 */
/datum/proc/ui_state(mob/user)
	return GLOB.default_state

/**
 * global
 *
 * Associative list of JSON-encoded shared states that were set by
 * tgui clients.
 */
/datum/var/list/tgui_shared_states

/**
 * global
 *
 * Tracks open UIs for a user.
 */
/mob/var/list/tgui_open_uis = list()

/**
 * global
 *
 * Tracks open windows for a user.
 */
/client/var/list/tgui_windows = list()

/**
 * global
 *
 * TRUE if cache was reloaded by tgui dev server at least once.
 */
/client/var/tgui_cache_reloaded = FALSE

/**
 * public
 *
 * Called on a UI's object when the UI is closed, not to be confused with
 * client/verb/uiclose(), which closes the ui window
 */
/datum/proc/ui_close(mob/user)
	SIGNAL_HANDLER

/**
 * verb
 *
 * Called by UIs when they are closed.
 * Must be a verb so winset() can call it.
 *
 * required uiref ref The UI that was closed.
 */
/client/verb/uiclose(window_id as text)
	// Name the verb, and hide it from the user panel.
	set name = "uiclose"
	set hidden = TRUE
	var/mob/user = src?.mob
	if(!user)
		return
	// Close all tgui datums based on window_id.
	SStgui.force_close_window(user, window_id)

/**
 * Middleware for /client/Topic.
 *
 * return bool If TRUE, prevents propagation of the topic call.
 */
/proc/tgui_Topic(href_list)
	// Skip non-tgui topics
	if(!href_list["tgui"])
		return FALSE
	var/type = href_list["type"]
	// Unconditionally collect tgui logs
	if(type == "log")
		var/context = href_list["window_id"]
		if (href_list["ns"])
			context += " ([href_list["ns"]])"
		log_tgui(usr, href_list["message"],
			context = context)
	// Reload all tgui windows
	if(type == "cacheReloaded")
		if(!check_rights(R_ADMIN) || usr.client.tgui_cache_reloaded)
			return TRUE
		// Mark as reloaded
		usr.client.tgui_cache_reloaded = TRUE
		// Notify windows
		var/list/windows = usr.client.tgui_windows
		for(var/window_id in windows)
			var/datum/tgui_window/window = windows[window_id]
			if (window.status == TGUI_WINDOW_READY)
				window.on_message(type, null, href_list)
		return TRUE
	// Locate window
	var/window_id = href_list["window_id"]
	var/datum/tgui_window/window
	if(window_id)
		window = usr.client.tgui_windows[window_id]
		if(!window)
			log_tgui(usr,
				"Error: Couldn't find the window datum, force closing.",
				context = window_id)
			SStgui.force_close_window(usr, window_id)
			return TRUE
	// Decode payload
	var/payload
	if(href_list["payload"])
		payload = json_decode(href_list["payload"])
	// Pass message to window
	if(window)
		window.on_message(type, payload, href_list)
	return TRUE


code:
/*!
 * Base state and helpers for states. Just does some sanity checks,
 * implement a proper state for in-depth checks.
 *
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * public
 *
 * Checks the UI state for a mob.
 *
 * required user mob The mob who opened/is using the UI.
 * required state datum/ui_state The state to check.
 *
 * return UI_state The state of the UI.
 */
/datum/proc/ui_status(mob/user, datum/ui_state/state)
	var/src_object = ui_host(user)
	. = UI_CLOSE
	if(!state)
		return

	if(isobserver(user))
		// If they turn on ghost AI control, admins can always interact.
		if(isAdminGhostAI(user))
			. = max(., UI_INTERACTIVE)

		// Regular ghosts can always at least view if in range.
		if(user.client)
			var/clientviewlist = getviewsize(user.client.view)
			if(get_dist(src_object, user) < max(clientviewlist[1], clientviewlist[2]))
				. = max(., UI_UPDATE)

	// Check if the state allows interaction
	var/result = state.can_use_topic(src_object, user)
	. = max(., result)

/**
 * private
 *
 * Checks if a user can use src_object's UI, and returns the state.
 * Can call a mob proc, which allows overrides for each mob.
 *
 * required src_object datum The object/datum which owns the UI.
 * required user mob The mob who opened/is using the UI.
 *
 * return UI_state The state of the UI.
 */
/datum/ui_state/proc/can_use_topic(src_object, mob/user)
	// Don't allow interaction by default.
	return UI_CLOSE

/**
 * public
 *
 * Standard interaction/sanity checks. Different mob types may have overrides.
 *
 * return UI_state The state of the UI.
 */
/mob/proc/shared_ui_interaction(src_object)
	// Close UIs if mindless.
	if(!client && !HAS_TRAIT(src, TRAIT_PRESERVE_UI_WITHOUT_CLIENT))
		return UI_CLOSE
	// Disable UIs if unconcious.
	/* SEPTIC EDIT REMOVAL
	else if(stat)
		return UI_DISABLED
	*/
	//SEPTIC EDIT BEGIN
	else if(stat > SOFT_CRIT)
		return UI_DISABLED
	//SEPTIC EDIT END
	// Update UIs if incapicitated but concious.
	else if(incapacitated())
		return UI_UPDATE
	return UI_INTERACTIVE

/mob/living/shared_ui_interaction(src_object)
	. = ..()
	if(!(mobility_flags & MOBILITY_UI) && . == UI_INTERACTIVE)
		return UI_UPDATE

/mob/living/silicon/ai/shared_ui_interaction(src_object)
	// Disable UIs if the AI is unpowered.
	if(apc_override == src_object) //allows AI to (eventually) use the interface for their own APC even when out of power
		return UI_INTERACTIVE
	if(lacks_power())
		return UI_DISABLED
	return ..()

/mob/living/silicon/robot/shared_ui_interaction(src_object)
	// Disable UIs if the object isn't installed in the borg AND the borg is either locked, has a dead cell, or no cell.
	var/atom/device = src_object
	if((istype(device) && device.loc != src) && (!cell || cell.charge <= 0 || lockcharge))
		return UI_DISABLED
	return ..()

/**
 * public
 *
 * Distance versus interaction check.
 *
 * required src_object atom/movable The object which owns the UI.
 *
 * return UI_state The state of the UI.
 */
/mob/living/proc/shared_living_ui_distance(atom/movable/src_object, viewcheck = TRUE)
	// If the object is obscured, close it.
	if(viewcheck && !(src_object in view(src)))
		return UI_CLOSE
	var/dist = get_dist(src_object, src)
	// Open and interact if 1-0 tiles away.
	if(dist <= 1)
		return UI_INTERACTIVE
	// View only if 2-3 tiles away.
	else if(dist <= 2)
		return UI_UPDATE
	// Disable if 5 tiles away.
	else if(dist <= 5)
		return UI_DISABLED
	// Otherwise, we got nothing.
	return UI_CLOSE

/mob/living/carbon/human/shared_living_ui_distance(atom/movable/src_object, viewcheck = TRUE)
	if(dna.check_mutation(TK) && tkMaxRangeCheck(src, src_object))
		return UI_INTERACTIVE
	return ..()


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: admin_state
 *
 * Checks that the user is an admin, end-of-story.
 */

GLOBAL_DATUM_INIT(admin_state, /datum/ui_state/admin_state, new)

/datum/ui_state/admin_state/can_use_topic(src_object, mob/user)
	if(check_rights_for(user.client, R_ADMIN))
		return UI_INTERACTIVE
	return UI_CLOSE


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: always_state
 *
 * Always grants the user UI_INTERACTIVE. Period.
 */

GLOBAL_DATUM_INIT(always_state, /datum/ui_state/always_state, new)

/datum/ui_state/always_state/can_use_topic(src_object, mob/user)
	return UI_INTERACTIVE


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: conscious_state
 *
 * Only checks if the user is conscious.
 */

GLOBAL_DATUM_INIT(conscious_state, /datum/ui_state/conscious_state, new)

/datum/ui_state/conscious_state/can_use_topic(src_object, mob/user)
	if(user.stat == CONSCIOUS)
		return UI_INTERACTIVE
	return UI_CLOSE


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: contained_state
 *
 * Checks that the user is inside the src_object.
 */

GLOBAL_DATUM_INIT(contained_state, /datum/ui_state/contained_state, new)

/datum/ui_state/contained_state/can_use_topic(atom/src_object, mob/user)
	if(!src_object.contains(user))
		return UI_CLOSE
	return user.shared_ui_interaction(src_object)


code:
GLOBAL_DATUM_INIT(debug_state, /datum/ui_state/debug_state, new)

/datum/ui_state/debug_state/can_use_topic(src_object, mob/user)
	if(check_rights_for(user.client, R_DEBUG))
		return UI_INTERACTIVE
	return UI_CLOSE


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: deep_inventory_state
 *
 * Checks that the src_object is in the user's deep
 * (backpack, box, toolbox, etc) inventory.
 */

GLOBAL_DATUM_INIT(deep_inventory_state, /datum/ui_state/deep_inventory_state, new)

/datum/ui_state/deep_inventory_state/can_use_topic(src_object, mob/user)
	if(!user.contains(src_object))
		return UI_CLOSE
	return user.shared_ui_interaction(src_object)


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: default_state
 *
 * Checks a number of things -- mostly physical distance for humans
 * and view for robots.
 */

GLOBAL_DATUM_INIT(default_state, /datum/ui_state/default, new)

/datum/ui_state/default/can_use_topic(src_object, mob/user)
	return user.default_can_use_topic(src_object) // Call the individual mob-overridden procs.

/mob/proc/default_can_use_topic(src_object)
	return UI_CLOSE // Don't allow interaction by default.

/mob/living/default_can_use_topic(src_object)
	. = shared_ui_interaction(src_object)
	if(. > UI_CLOSE && loc) //must not be in nullspace.
		. = min(., shared_living_ui_distance(src_object)) // Check the distance...
	if(. == UI_INTERACTIVE && !ISADVANCEDTOOLUSER(src)) // unhandy living mobs can only look, not touch.
		return UI_UPDATE

/mob/living/silicon/robot/default_can_use_topic(src_object)
	. = shared_ui_interaction(src_object)
	if(. <= UI_DISABLED)
		return

	// Robots can interact with anything they can see.
	var/list/clientviewlist = getviewsize(client.view)
	if(get_dist(src, src_object) <= min(clientviewlist[1],clientviewlist[2]))
		return UI_INTERACTIVE
	return UI_DISABLED // Otherwise they can keep the UI open.

/mob/living/silicon/ai/default_can_use_topic(src_object)
	. = shared_ui_interaction(src_object)
	if(. < UI_INTERACTIVE)
		return

	// The AI can interact with anything it can see nearby, or with cameras while wireless control is enabled.
	if(!control_disabled && can_see(src_object))
		return UI_INTERACTIVE
	return UI_CLOSE

/mob/living/silicon/pai/default_can_use_topic(src_object)
	// pAIs can only use themselves and the owner's radio.
	if((src_object == src || src_object == radio) && !stat)
		return UI_INTERACTIVE
	else
		return min(..(), UI_UPDATE)


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: hands_state
 *
 * Checks that the src_object is in the user's hands.
 */

GLOBAL_DATUM_INIT(hands_state, /datum/ui_state/hands_state, new)

/datum/ui_state/hands_state/can_use_topic(src_object, mob/user)
	. = user.shared_ui_interaction(src_object)
	if(. > UI_CLOSE)
		return min(., user.hands_can_use_topic(src_object))

/mob/proc/hands_can_use_topic(src_object)
	return UI_CLOSE

/mob/living/hands_can_use_topic(src_object)
	if(is_holding(src_object))
		return UI_INTERACTIVE
	return UI_CLOSE

/mob/living/silicon/robot/hands_can_use_topic(src_object)
	if(activated(src_object))
		return UI_INTERACTIVE
	return UI_CLOSE


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: human_adjacent_state
 *
 * In addition to default checks, only allows interaction for a
 * human adjacent user.
 */

GLOBAL_DATUM_INIT(human_adjacent_state, /datum/ui_state/human_adjacent_state, new)

/datum/ui_state/human_adjacent_state/can_use_topic(src_object, mob/user)
	. = user.default_can_use_topic(src_object)

	var/dist = get_dist(src_object, user)
	if((dist > 1) || (!ishuman(user)))
		// Can't be used unless adjacent and human, even with TK
		. = min(., UI_UPDATE)


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: inventory_state
 *
 * Checks that the src_object is in the user's top-level
 * (hand, ear, pocket, belt, etc) inventory.
 */

GLOBAL_DATUM_INIT(inventory_state, /datum/ui_state/inventory_state, new)

/datum/ui_state/inventory_state/can_use_topic(src_object, mob/user)
	if(!(src_object in user))
		return UI_CLOSE
	return user.shared_ui_interaction(src_object)


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: language_menu_state
 */

GLOBAL_DATUM_INIT(language_menu_state, /datum/ui_state/language_menu, new)

/datum/ui_state/language_menu/can_use_topic(src_object, mob/user)
	. = UI_CLOSE
	if(check_rights_for(user.client, R_ADMIN))
		. = UI_INTERACTIVE
	else if(istype(src_object, /datum/language_menu))
		var/datum/language_menu/LM = src_object
		if(LM.language_holder.get_atom() == user)
			. = UI_INTERACTIVE


code:
/*!
 * Copyright (c) 2021 Arm A. Hammer
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: never_state
 *
 * Always closes the UI, no matter what. See the ui_state in religious_tool.dm to see an example
 */

GLOBAL_DATUM_INIT(never_state, /datum/ui_state/never_state, new)

/datum/ui_state/never_state/can_use_topic(src_object, mob/user)
	return UI_CLOSE


code:
/**
 * tgui state: new_player_state
 *
 * Checks that the user is a new_player, or if user is an admin
 */

GLOBAL_DATUM_INIT(new_player_state, /datum/ui_state/new_player_state, new)

/datum/ui_state/new_player_state/can_use_topic(src_object, mob/user)
	if(isnewplayer(user) || check_rights_for(user.client, R_ADMIN))
		return UI_INTERACTIVE
	return UI_CLOSE


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: not_incapacitated_state
 *
 * Checks that the user isn't incapacitated
 */

GLOBAL_DATUM_INIT(not_incapacitated_state, /datum/ui_state/not_incapacitated_state, new)

/**
 * tgui state: not_incapacitated_turf_state
 *
 * Checks that the user isn't incapacitated and that their loc is a turf
 */

GLOBAL_DATUM_INIT(not_incapacitated_turf_state, /datum/ui_state/not_incapacitated_state, new(no_turfs = TRUE))

/datum/ui_state/not_incapacitated_state
	var/turf_check = FALSE

/datum/ui_state/not_incapacitated_state/New(loc, no_turfs = FALSE)
	..()
	turf_check = no_turfs

/datum/ui_state/not_incapacitated_state/can_use_topic(src_object, mob/user)
	if(user.stat != CONSCIOUS)
		return UI_CLOSE
	if(HAS_TRAIT(src, TRAIT_UI_BLOCKED) || user.incapacitated() || (turf_check && !isturf(user.loc)))
		return UI_DISABLED
	return UI_INTERACTIVE


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: notcontained_state
 *
 * Checks that the user is not inside src_object, and then makes the
 * default checks.
 */

GLOBAL_DATUM_INIT(notcontained_state, /datum/ui_state/notcontained_state, new)

/datum/ui_state/notcontained_state/can_use_topic(atom/src_object, mob/user)
	. = user.shared_ui_interaction(src_object)
	if(. > UI_CLOSE)
		return min(., user.notcontained_can_use_topic(src_object))

/mob/proc/notcontained_can_use_topic(src_object)
	return UI_CLOSE

/mob/living/notcontained_can_use_topic(atom/src_object)
	if(src_object.contains(src))
		return UI_CLOSE // Close if we're inside it.
	return default_can_use_topic(src_object)

/mob/living/silicon/notcontained_can_use_topic(src_object)
	return default_can_use_topic(src_object) // Silicons use default bevhavior.

/mob/living/simple_animal/drone/notcontained_can_use_topic(src_object)
	return default_can_use_topic(src_object) // Drones use default bevhavior.


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: observer_state
 *
 * Checks that the user is an observer/ghost.
 */

GLOBAL_DATUM_INIT(observer_state, /datum/ui_state/observer_state, new)

/datum/ui_state/observer_state/can_use_topic(src_object, mob/user)
	if(isobserver(user))
		return UI_INTERACTIVE
	return UI_CLOSE


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: physical_state
 *
 * Short-circuits the default state to only check physical distance.
 */

GLOBAL_DATUM_INIT(physical_state, /datum/ui_state/physical, new)

/datum/ui_state/physical/can_use_topic(src_object, mob/user)
	. = user.shared_ui_interaction(src_object)
	if(. > UI_CLOSE)
		return min(., user.physical_can_use_topic(src_object))

/mob/proc/physical_can_use_topic(src_object)
	return UI_CLOSE

/mob/living/physical_can_use_topic(src_object)
	return shared_living_ui_distance(src_object)

/mob/living/silicon/physical_can_use_topic(src_object)
	return max(UI_UPDATE, shared_living_ui_distance(src_object)) // Silicons can always see.

/mob/living/silicon/ai/physical_can_use_topic(src_object)
	return UI_UPDATE // AIs are not physical.

/**
 * tgui state: physical_obscured_state
 *
 * Short-circuits the default state to only check physical distance, being in view doesn't matter
 */

GLOBAL_DATUM_INIT(physical_obscured_state, /datum/ui_state/physical_obscured_state, new)

/datum/ui_state/physical_obscured_state/can_use_topic(src_object, mob/user)
	. = user.shared_ui_interaction(src_object)
	if(. > UI_CLOSE)
		return min(., user.physical_obscured_can_use_topic(src_object))

/mob/proc/physical_obscured_can_use_topic(src_object)
	return UI_CLOSE

/mob/living/physical_obscured_can_use_topic(src_object)
	return shared_living_ui_distance(src_object, viewcheck = FALSE)

/mob/living/silicon/physical_obscured_can_use_topic(src_object)
	return max(UI_UPDATE, shared_living_ui_distance(src_object, viewcheck = FALSE)) // Silicons can always see.

/mob/living/silicon/ai/physical_obscured_can_use_topic(src_object)
	return UI_UPDATE // AIs are not physical.


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: self_state
 *
 * Only checks that the user and src_object are the same.
 */

GLOBAL_DATUM_INIT(self_state, /datum/ui_state/self_state, new)

/datum/ui_state/self_state/can_use_topic(src_object, mob/user)
	if(src_object != user)
		return UI_CLOSE
	return user.shared_ui_interaction(src_object)


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui state: z_state
 *
 * Only checks that the Z-level of the user and src_object are the same.
 */

GLOBAL_DATUM_INIT(z_state, /datum/ui_state/z_state, new)

/datum/ui_state/z_state/can_use_topic(src_object, mob/user)
	var/turf/turf_obj = get_turf(src_object)
	var/turf/turf_usr = get_turf(user)
	if(turf_obj && turf_usr && turf_obj.z == turf_usr.z)
		return UI_INTERACTIVE
	return UI_CLOSE


code:
/// The sane defaults for a UI such as a computer or a machine.
/proc/default_ui_state(mob/user, atom/source)
	return min(
		ui_status_user_is_abled(user, source),
		ui_status_user_has_free_hands(user, source),
		ui_status_user_is_advanced_tool_user(user),
		ui_status_only_living(user),
		max(
			ui_status_user_is_adjacent(user, source),
			ui_status_silicon_has_access(user, source),
		)
	)

/// Returns a UI status such that users adjacent to source will be able to interact,
/// far away users will be able to see, and anyone farther won't see anything.
/// Dead users will receive updates no matter what, though you likely want to add
/// a [`ui_status_only_living`] check for finer observer interactions.
/proc/ui_status_user_is_adjacent(mob/user, atom/source)
	if (isliving(user))
		var/mob/living/living_user = user
		return living_user.shared_living_ui_distance(source)
	else
		return UI_UPDATE

/// Returns a UI status such that the dead will be able to watch, but not interact.
/proc/ui_status_only_living(mob/user, source)
	if (isliving(user))
		return UI_INTERACTIVE

	if(isobserver(user))
		// If they turn on ghost AI control, admins can always interact.
		if(isAdminGhostAI(user))
			return UI_INTERACTIVE

		// Regular ghosts can always at least view if in range.
		var/datum/client_interface/client = GET_CLIENT(user)
		if(client)
			var/clientviewlist = getviewsize(client.view)
			if(get_dist(source, user) < max(clientviewlist[1], clientviewlist[2]))
				return UI_UPDATE

	return UI_CLOSE

/// Returns a UI status such that users with debilitating conditions, such as
/// being dead or not having power for silicons, will not be able to interact.
/// Being dead will disable UI, being incapacitated will continue updating it,
/// and anything else will make it interactive.
/proc/ui_status_user_is_abled(mob/user, atom/source)
	return user.shared_ui_interaction(source)

/// Returns a UI status such that those without blocked hands will be able to interact,
/// but everyone else can only watch.
/proc/ui_status_user_has_free_hands(mob/user, atom/source)
	return HAS_TRAIT(user, TRAIT_HANDS_BLOCKED) ? UI_UPDATE : UI_INTERACTIVE

/// Returns a UI status such that advanced tool users will be able to interact,
/// but everyone else can only watch.
/proc/ui_status_user_is_advanced_tool_user(mob/user)
	return ISADVANCEDTOOLUSER(user) ? UI_INTERACTIVE : UI_UPDATE

/// Returns a UI status such that silicons will be able to interact with whatever
/// they would have access to if this was a machine. For example, AIs can
/// interact if there's cameras with wireless control is enabled.
/proc/ui_status_silicon_has_access(mob/user, atom/source)
	if (!issilicon(user))
		return UI_CLOSE
	var/mob/living/silicon/silicon_user = user
	return silicon_user.get_ui_access(source)

/// Returns a UI status representing this silicon's capability to access
/// the given source. Called by `ui_status_silicon_has_access`.
/mob/living/silicon/proc/get_ui_access(atom/source)
	return UI_CLOSE

/mob/living/silicon/robot/get_ui_access(atom/source)
	// Robots can interact with anything they can see.
	var/list/clientviewlist = getviewsize(client.view)
	if(get_dist(src, source) <= min(clientviewlist[1],clientviewlist[2]))
		return UI_INTERACTIVE
	return UI_DISABLED // Otherwise they can keep the UI open.

/mob/living/silicon/ai/get_ui_access(atom/source)
	// The AI can interact with anything it can see nearby, or with cameras while wireless control is enabled.
	if(!control_disabled && can_see(source))
		return UI_INTERACTIVE
	return UI_CLOSE

/mob/living/silicon/pai/get_ui_access(atom/source)
	// pAIs can only use themselves and the owner's radio.
	if((source == src || source == radio) && !stat)
		return UI_INTERACTIVE
	else
		return UI_CLOSE

/// Returns UI_INTERACTIVE if the user is conscious and lying down.
/// Returns UI_UPDATE otherwise.
/proc/ui_status_user_is_conscious_and_lying_down(mob/user)
	if (!isliving(user))
		return UI_UPDATE

	var/mob/living/living_user = user
	return (living_user.body_position == LYING_DOWN && living_user.stat == CONSCIOUS) \
		? UI_INTERACTIVE \
		: UI_UPDATE

/// Return UI_INTERACTIVE if the user is strictly adjacent to the target atom, whether they can see it or not.
/// Return UI_CLOSE otherwise.
/proc/ui_status_user_strictly_adjacent(mob/user, atom/target)
	if(get_dist(target, user) > 1)
		return UI_CLOSE

	return UI_INTERACTIVE


code:
/*!
 * Copyright (c) 2020 Aleksej Komarov
 * SPDX-License-Identifier: MIT
 */

/**
 * tgui datum (represents a UI).
 */
/datum/tgui
	/// The mob who opened/is using the UI.
	var/mob/user
	/// The object which owns the UI.
	var/datum/src_object
	/// The title of te UI.
	var/title
	/// The window_id for browse() and onclose().
	var/datum/tgui_window/window
	/// Key that is used for remembering the window geometry.
	var/window_key
	/// Deprecated: Window size.
	var/window_size
	/// The interface (template) to be used for this UI.
	var/interface
	/// Update the UI every MC tick.
	var/autoupdate = TRUE
	/// If the UI has been initialized yet.
	var/initialized = FALSE
	/// Time of opening the window.
	var/opened_at
	/// Stops further updates when close() was called.
	var/closing = FALSE
	/// The status/visibility of the UI.
	var/status = UI_INTERACTIVE
	/// Topic state used to determine status/interactability.
	var/datum/ui_state/state = null

/**
 * public
 *
 * Create a new UI.
 *
 * required user mob The mob who opened/is using the UI.
 * required src_object datum The object or datum which owns the UI.
 * required interface string The interface used to render the UI.
 * optional title string The title of the UI.
 * optional ui_x int Deprecated: Window width.
 * optional ui_y int Deprecated: Window height.
 *
 * return datum/tgui The requested UI.
 */
/datum/tgui/New(mob/user, datum/src_object, interface, title, ui_x, ui_y)
	log_tgui(user,
		"new [interface] fancy [user?.client?.prefs.read_preference(/datum/preference/toggle/tgui_fancy)]",
		src_object = src_object)
	src.user = user
	src.src_object = src_object
	src.window_key = "[REF(src_object)]-main"
	src.interface = interface
	if(title)
		src.title = title
	src.state = src_object.ui_state(user)
	// Deprecated
	if(ui_x && ui_y)
		src.window_size = list(ui_x, ui_y)

/datum/tgui/Destroy()
	user = null
	src_object = null
	return ..()

/**
 * public
 *
 * Open this UI (and initialize it with data).
 *
 * return bool - TRUE if a new pooled window is opened, FALSE in all other situations including if a new pooled window didn't open because one already exists.
 */
/datum/tgui/proc/open()
	if(!user.client)
		return FALSE
	if(window)
		return FALSE
	process_status()
	if(status < UI_UPDATE)
		return FALSE
	window = SStgui.request_pooled_window(user)
	if(!window)
		return FALSE
	opened_at = world.time
	window.acquire_lock(src)
	if(!window.is_ready())
		window.initialize(
			fancy = user.client.prefs.read_preference(/datum/preference/toggle/tgui_fancy),
			inline_assets = list(
				get_asset_datum(/datum/asset/simple/tgui),
			))
	else
		window.send_message("ping")
	var/flush_queue = window.send_asset(get_asset_datum(
		/datum/asset/simple/namespaced/fontawesome))
	flush_queue |= window.send_asset(get_asset_datum(
		/datum/asset/simple/namespaced/tgfont))
	for(var/datum/asset/asset in src_object.ui_assets(user))
		flush_queue |= window.send_asset(asset)
	if (flush_queue)
		user.client.browse_queue_flush()
	window.send_message("update", get_payload(
		with_data = TRUE,
		with_static_data = TRUE))
	SStgui.on_open(src)

	return TRUE

/**
 * public
 *
 * Close the UI.
 *
 * optional can_be_suspended bool
 */
/datum/tgui/proc/close(can_be_suspended = TRUE)
	if(closing)
		return
	closing = TRUE
	// If we don't have window_id, open proc did not have the opportunity
	// to finish, therefore it's safe to skip this whole block.
	if(window)
		// Windows you want to keep are usually blue screens of death
		// and we want to keep them around, to allow user to read
		// the error message properly.
		window.release_lock()
		window.close(can_be_suspended)
		src_object.ui_close(user)
		SStgui.on_close(src)
	state = null
	qdel(src)

/**
 * public
 *
 * Enable/disable auto-updating of the UI.
 *
 * required value bool Enable/disable auto-updating.
 */
/datum/tgui/proc/set_autoupdate(autoupdate)
	src.autoupdate = autoupdate

/**
 * public
 *
 * Replace current ui.state with a new one.
 *
 * required state datum/ui_state/state Next state
 */
/datum/tgui/proc/set_state(datum/ui_state/state)
	src.state = state

/**
 * public
 *
 * Makes an asset available to use in tgui.
 *
 * required asset datum/asset
 *
 * return bool - true if an asset was actually sent
 */
/datum/tgui/proc/send_asset(datum/asset/asset)
	if(!window)
		CRASH("send_asset() was called either without calling open() first or when open() did not return TRUE.")
	return window.send_asset(asset)

/**
 * public
 *
 * Send a full update to the client (includes static data).
 *
 * optional custom_data list Custom data to send instead of ui_data.
 * optional force bool Send an update even if UI is not interactive.
 */
/datum/tgui/proc/send_full_update(custom_data, force)
	if(!user.client || !initialized || closing)
		return
	var/should_update_data = force || status >= UI_UPDATE
	window.send_message("update", get_payload(
		custom_data,
		with_data = should_update_data,
		with_static_data = TRUE))

/**
 * public
 *
 * Send a partial update to the client (excludes static data).
 *
 * optional custom_data list Custom data to send instead of ui_data.
 * optional force bool Send an update even if UI is not interactive.
 */
/datum/tgui/proc/send_update(custom_data, force)
	if(!user.client || !initialized || closing)
		return
	var/should_update_data = force || status >= UI_UPDATE
	window.send_message("update", get_payload(
		custom_data,
		with_data = should_update_data))

/**
 * private
 *
 * Package the data to send to the UI, as JSON.
 *
 * return list
 */
/datum/tgui/proc/get_payload(custom_data, with_data, with_static_data)
	var/list/json_data = list()
	json_data["config"] = list(
		"title" = title,
		"status" = status,
		"interface" = interface,
		"window" = list(
			"key" = window_key,
			"size" = window_size,
			"fancy" = user.client.prefs.read_preference(/datum/preference/toggle/tgui_fancy),
			"locked" = user.client.prefs.read_preference(/datum/preference/toggle/tgui_lock),
		),
		"client" = list(
			"ckey" = user.client.ckey,
			"address" = user.client.address,
			"computer_id" = user.client.computer_id,
		),
		"user" = list(
			"name" = "[user]",
			"observer" = isobserver(user),
		),
	)
	var/data = custom_data || with_data && src_object.ui_data(user)
	if(data)
		json_data["data"] = data
	var/static_data = with_static_data && src_object.ui_static_data(user)
	if(static_data)
		json_data["static_data"] = static_data
	if(src_object.tgui_shared_states)
		json_data["shared"] = src_object.tgui_shared_states
	return json_data

/**
 * private
 *
 * Run an update cycle for this UI. Called internally by SStgui
 * every second or so.
 */
/datum/tgui/process(delta_time, force = FALSE)
	if(closing)
		return
	var/datum/host = src_object.ui_host(user)
	// If the object or user died (or something else), abort.
	if(!src_object || !host || !user || !window)
		close(can_be_suspended = FALSE)
		return
	// Validate ping
	if(!initialized && world.time - opened_at > TGUI_PING_TIMEOUT)
		log_tgui(user, "Error: Zombie window detected, closing.",
			window = window,
			src_object = src_object)
		close(can_be_suspended = FALSE)
		return
	// Update through a normal call to ui_interact
	if(status != UI_DISABLED && (autoupdate || force))
		src_object.ui_interact(user, src)
		return
	// Update status only
	var/needs_update = process_status()
	if(status <= UI_CLOSE)
		close()
		return
	if(needs_update)
		window.send_message("update", get_payload())

/**
 * private
 *
 * Updates the status, and returns TRUE if status has changed.
 */
/datum/tgui/proc/process_status()
	var/prev_status = status
	status = src_object.ui_status(user, state)
	return prev_status != status

/**
 * private
 *
 * Callback for handling incoming tgui messages.
 */
/datum/tgui/proc/on_message(type, list/payload, list/href_list)
	// Pass act type messages to ui_act
	if(type && copytext(type, 1, 5) == "act/")
		var/act_type = copytext(type, 5)
		log_tgui(user, "Action: [act_type] [href_list["payload"]]",
			window = window,
			src_object = src_object)
		process_status()
		if(src_object.ui_act(act_type, payload, src, state))
			SStgui.update_uis(src_object)
		return FALSE
	switch(type)
		if("ready")
			initialized = TRUE
		if("pingReply")
			initialized = TRUE
		if("suspend")
			close(can_be_suspended = TRUE)
		if("close")
			close(can_be_suspended = FALSE)
		if("log")
			if(href_list["fatal"])
				close(can_be_suspended = FALSE)
		if("setSharedState")
			if(status != UI_INTERACTIVE)
				return
			LAZYINITLIST(src_object.tgui_shared_states)
			src_object.tgui_shared_states[href_list["key"]] = href_list["value"]
			SStgui.update_uis(src_object)


code:
/**
 * Creates a TGUI alert window and returns the user's response.
 *
 * This proc should be used to create alerts that the caller will wait for a response from.
 * Arguments:
 * * user - The user to show the alert to.
 * * message - The content of the alert, shown in the body of the TGUI window.
 * * title - The of the alert modal, shown on the top of the TGUI window.
 * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
 * * timeout - The timeout of the alert, after which the modal will close and qdel itself. Set to zero for no timeout.
 * * autofocus - The bool that controls if this alert should grab window focus.
 */
/proc/tgui_alert(mob/user, message = null, title = null, list/buttons = list("Ok"), timeout = 0, autofocus = TRUE)
	if (!user)
		user = usr
	if (!istype(user))
		if (istype(user, /client))
			var/client/client = user
			user = client.mob
		else
			return
	var/datum/tgui_modal/alert = new(user, message, title, buttons, timeout, autofocus)
	alert.ui_interact(user)
	alert.wait()
	if (alert)
		. = alert.choice
		qdel(alert)

/**
 * Creates an asynchronous TGUI alert window with an associated callback.
 *
 * This proc should be used to create alerts that invoke a callback with the user's chosen option.
 * Arguments:
 * * user - The user to show the alert to.
 * * message - The content of the alert, shown in the body of the TGUI window.
 * * title - The of the alert modal, shown on the top of the TGUI window.
 * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
 * * callback - The callback to be invoked when a choice is made.
 * * timeout - The timeout of the alert, after which the modal will close and qdel itself. Disabled by default, can be set to seconds otherwise.
 * * autofocus - The bool that controls if this alert should grab window focus.
 */
/proc/tgui_alert_async(mob/user, message = null, title = null, list/buttons = list("Ok"), datum/callback/callback, timeout = 0, autofocus = TRUE)
	if (!user)
		user = usr
	if (!istype(user))
		if (istype(user, /client))
			var/client/client = user
			user = client.mob
		else
			return
	var/datum/tgui_modal/async/alert = new(user, message, title, buttons, callback, timeout, autofocus)
	alert.ui_interact(user)

/**
 * # tgui_modal
 *
 * Datum used for instantiating and using a TGUI-controlled modal that prompts the user with
 * a message and has buttons for responses.
 */
/datum/tgui_modal
	/// The title of the TGUI window
	var/title
	/// The textual body of the TGUI window
	var/message
	/// The list of buttons (responses) provided on the TGUI window
	var/list/buttons
	/// The button that the user has pressed, null if no selection has been made
	var/choice
	/// The time at which the tgui_modal was created, for displaying timeout progress.
	var/start_time
	/// The lifespan of the tgui_modal, after which the window will close and delete itself.
	var/timeout
	/// The bool that controls if this modal should grab window focus
	var/autofocus
	/// Boolean field describing if the tgui_modal was closed by the user.
	var/closed

/datum/tgui_modal/New(mob/user, message, title, list/buttons, timeout, autofocus)
	src.title = title
	src.message = message
	src.buttons = buttons.Copy()
	src.autofocus = autofocus
	if (timeout)
		src.timeout = timeout
		start_time = world.time
		QDEL_IN(src, timeout)

/datum/tgui_modal/Destroy(force, ...)
	SStgui.close_uis(src)
	QDEL_NULL(buttons)
	. = ..()

/**
 * Waits for a user's response to the tgui_modal's prompt before returning. Returns early if
 * the window was closed by the user.
 */
/datum/tgui_modal/proc/wait()
	while (!choice && !closed && !QDELETED(src))
		stoplag(1)

/datum/tgui_modal/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "AlertModal")
		ui.open()

/datum/tgui_modal/ui_close(mob/user)
	. = ..()
	closed = TRUE

/datum/tgui_modal/ui_state(mob/user)
	return GLOB.always_state

/datum/tgui_modal/ui_data(mob/user)
	. = list(
		"title" = title,
		"message" = message,
		"buttons" = buttons,
		"autofocus" = autofocus
	)

	if(timeout)
		.["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))

/datum/tgui_modal/ui_act(action, list/params)
	. = ..()
	if (.)
		return
	switch(action)
		if("choose")
			if (!(params["choice"] in buttons))
				return
			set_choice(params["choice"])
			SStgui.close_uis(src)
			return TRUE

/datum/tgui_modal/proc/set_choice(choice)
	src.choice = choice

/**
 * # async tgui_modal
 *
 * An asynchronous version of tgui_modal to be used with callbacks instead of waiting on user responses.
 */
/datum/tgui_modal/async
	/// The callback to be invoked by the tgui_modal upon having a choice made.
	var/datum/callback/callback

/datum/tgui_modal/async/New(mob/user, message, title, list/buttons, callback, timeout, autofocus)
	..(user, message, title, buttons, timeout, autofocus)
	src.callback = callback

/datum/tgui_modal/async/Destroy(force, ...)
	QDEL_NULL(callback)
	. = ..()

/datum/tgui_modal/async/set_choice(choice)
	. = ..()
	if(!isnull(src.choice))
		callback?.InvokeAsync(src.choice)

/datum/tgui_modal/async/wait()
	return


code:
/**
 * Creates a TGUI input list window and returns the user's response.
 *
 * This proc should be used to create alerts that the caller will wait for a response from.
 * Arguments:
 * * user - The user to show the input box to.
 * * message - The content of the input box, shown in the body of the TGUI window.
 * * title - The title of the input box, shown on the top of the TGUI window.
 * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
 * * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout.
 */
/proc/tgui_input_list(mob/user, message, title, list/buttons, timeout = 0)
	if (!user)
		user = usr
	if(!length(buttons))
		return
	if (!istype(user))
		if (istype(user, /client))
			var/client/client = user
			user = client.mob
		else
			return
	var/datum/tgui_list_input/input = new(user, message, title, buttons, timeout)
	input.ui_interact(user)
	input.wait()
	if (input)
		. = input.choice
		qdel(input)

/**
 * Creates an asynchronous TGUI input list window with an associated callback.
 *
 * This proc should be used to create inputs that invoke a callback with the user's chosen option.
 * Arguments:
 * * user - The user to show the input box to.
 * * message - The content of the input box, shown in the body of the TGUI window.
 * * title - The title of the input box, shown on the top of the TGUI window.
 * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
 * * callback - The callback to be invoked when a choice is made.
 * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
 */
/proc/tgui_input_list_async(mob/user, message, title, list/buttons, datum/callback/callback, timeout = 60 SECONDS)
	if (!user)
		user = usr
	if(!length(buttons))
		return
	if (!istype(user))
		if (istype(user, /client))
			var/client/client = user
			user = client.mob
		else
			return
	var/datum/tgui_list_input/async/input = new(user, message, title, buttons, callback, timeout)
	input.ui_interact(user)

/**
 * # tgui_list_input
 *
 * Datum used for instantiating and using a TGUI-controlled list input that prompts the user with
 * a message and shows a list of selectable options
 */
/datum/tgui_list_input
	/// The title of the TGUI window
	var/title
	/// The textual body of the TGUI window
	var/message
	/// The list of buttons (responses) provided on the TGUI window
	var/list/buttons
	/// Buttons (strings specifically) mapped to the actual value (e.g. a mob or a verb)
	var/list/buttons_map
	/// The button that the user has pressed, null if no selection has been made
	var/choice
	/// The time at which the tgui_list_input was created, for displaying timeout progress.
	var/start_time
	/// The lifespan of the tgui_list_input, after which the window will close and delete itself.
	var/timeout
	/// Boolean field describing if the tgui_list_input was closed by the user.
	var/closed

/datum/tgui_list_input/New(mob/user, message, title, list/buttons, timeout)
	src.title = title
	src.message = message
	src.buttons = list()
	src.buttons_map = list()
	var/list/repeat_buttons = list()

	// Gets rid of illegal characters
	var/static/regex/whitelistedWords = regex(@{"([^\u0020-\u8000]+)"})

	for(var/i in buttons)
		var/string_key = whitelistedWords.Replace("[i]", "")

		//avoids duplicated keys E.g: when areas have the same name
		string_key = avoid_assoc_duplicate_keys(string_key, repeat_buttons)

		src.buttons += string_key
		src.buttons_map[string_key] = i

	if (timeout)
		src.timeout = timeout
		start_time = world.time
		QDEL_IN(src, timeout)

/datum/tgui_list_input/Destroy(force, ...)
	SStgui.close_uis(src)
	QDEL_NULL(buttons)
	. = ..()

/**
 * Waits for a user's response to the tgui_list_input's prompt before returning. Returns early if
 * the window was closed by the user.
 */
/datum/tgui_list_input/proc/wait()
	while (!choice && !closed)
		stoplag(1)

/datum/tgui_list_input/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "ListInput")
		ui.open()

/datum/tgui_list_input/ui_close(mob/user)
	. = ..()
	closed = TRUE

/datum/tgui_list_input/ui_state(mob/user)
	return GLOB.always_state

/datum/tgui_list_input/ui_static_data(mob/user)
	. = list(
		"title" = title,
		"message" = message,
		"buttons" = buttons
	)

/datum/tgui_list_input/ui_data(mob/user)
	. = list()
	if(timeout)
		.["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)

/datum/tgui_list_input/ui_act(action, list/params)
	. = ..()
	if (.)
		return
	switch(action)
		if("choose")
			if (!(params["choice"] in buttons))
				return
			set_choice(buttons_map[params["choice"]])
			SStgui.close_uis(src)
			return TRUE
		if("cancel")
			SStgui.close_uis(src)
			closed = TRUE
			return TRUE

/datum/tgui_list_input/proc/set_choice(choice)
	src.choice = choice

/**
 * # async tgui_list_input
 *
 * An asynchronous version of tgui_list_input to be used with callbacks instead of waiting on user responses.
 */
/datum/tgui_list_input/async
	/// The callback to be invoked by the tgui_list_input upon having a choice made.
	var/datum/callback/callback

/datum/tgui_list_input/async/New(mob/user, message, title, list/buttons, callback, timeout)
	..(user, message, title, buttons, timeout)
	src.callback = callback

/datum/tgui_list_input/async/Destroy(force, ...)
	QDEL_NULL(callback)
	. = ..()

/datum/tgui_list_input/async/set_choice(choice)
	. = ..()
	if(!isnull(src.choice))
		callback?.InvokeAsync(src.choice)

/datum/tgui_list_input/async/wait()
	return