code:
//SDQL2 datumized, /tg/station special!

/*
	Welcome admins, badmins and coders alike, to Structured Datum Query Language.
	SDQL allows you to powerfully run code on batches of objects (or single objects, it's still unmatched
	even there.)
	When I say "powerfully" I mean it you're in for a ride.

	Ok so say you want to get a list of every mob. How does one do this?
	"SELECT /mob"
	This will open a list of every object in world that is a /mob.
	And you can VV them if you need.

	What if you want to get every mob on a *specific z-level*?
	"SELECT /mob WHERE z == 4"

	What if you want to select every mob on even numbered z-levels?
	"SELECT /mob WHERE z % 2 == 0"

	Can you see where this is going? You can select objects with an arbitrary expression.
	These expressions can also do variable access and proc calls (yes, both on-object and globals!)
	Keep reading!

	Ok. What if you want to get every machine in the SSmachine process list? Looping through world is kinda
	slow.

	"SELECT * IN SSmachines.machinery"

	Here "*" as type functions as a wildcard.
	We know everything in the global SSmachines.machinery list is a machine.

	You can specify "IN <expression>" to return a list to operate on.
	This can be any list that you can wizard together from global variables and global proc calls.
	Every variable/proc name in the "IN" block is global.
	It can also be a single object, in which case the object is wrapped in a list for you.
	So yeah SDQL is unironically better than VV for complex single-object operations.

	You can of course combine these.
	"SELECT * IN SSmachines.machinery WHERE z == 4"
	"SELECT * IN SSmachines.machinery WHERE stat & 2" // (2 is NOPOWER, can't use defines from SDQL. Sorry!)
	"SELECT * IN SSmachines.machinery WHERE stat & 2 && z == 4"

	The possibilities are endless (just don't crash the server, ok?).

	Oh it gets better.

	You can use "MAP <expression>" to run some code per object and use the result. For example:

	"SELECT /obj/machinery/power/smes MAP [charge / capacity * 100, RCon_tag, src]"

	This will give you a list of all the APCs, their charge AND RCon tag. Useful eh?

	[] being a list here. Yeah you can write out lists directly without > lol lists in VV. Color matrix
	shenanigans inbound.

	After the "MAP" segment is executed, the rest of the query executes as if it's THAT object you just made
	(here the list).
	Yeah, by the way, you can chain these MAP / WHERE things FOREVER!

	"SELECT /mob WHERE client MAP client WHERE holder MAP holder"

	You can also generate a new list on the fly using a selector array. @[] will generate a list of objects based off the selector provided.

	"SELECT /mob/living IN (@[/area/service/bar MAP contents])[1]"

	What if some dumbass admin spawned a bajillion spiders and you need to kill them all?
	Oh yeah you'd rather not delete all the spiders in maintenace. Only that one room the spiders were
	spawned in.

	"DELETE /mob/living/carbon/superior_animal/giant_spider WHERE loc.loc == marked"

	Here I used VV to mark the area they were in, and since loc.loc = area, voila.
	Only the spiders in a specific area are gone.

	Or you know if you want to catch spiders that crawled into lockers too (how even?)

	"DELETE /mob/living/carbon/superior_animal/giant_spider WHERE global.get_area(src) == marked"

	What else can you do?

	Well suppose you'd rather gib those spiders instead of simply flat deleting them...

	"CALL gib() ON /mob/living/carbon/superior_animal/giant_spider WHERE global.get_area(src) == marked"

	Or you can have some fun..

	"CALL forceMove(marked) ON /mob/living/carbon/superior_animal"

	You can also run multiple queries sequentially:

	"CALL forceMove(marked) ON /mob/living/carbon/superior_animal; CALL gib() ON
	/mob/living/carbon/superior_animal"

	And finally, you can directly modify variables on objects.

	"UPDATE /mob WHERE client SET client.color = [0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]"

	Don't crash the server, OK?

	"UPDATE /mob/living/carbon/human/species/monkey SET #null = forceMove(usr.loc)"

	Writing "#null" in front of the "=" will call the proc and discard the return value.

	A quick recommendation: before you run something like a DELETE or another query.. Run it through SELECT
	first.
	You'd rather not gib every player on accident.
	Or crash the server.

	By the way, queries are slow and take a while. Be patient.
	They don't hang the entire server though.

	With great power comes great responsability.

	Here's a slightly more formal quick reference.

	The 4 queries you can do are:

	"SELECT <selectors>"
	"CALL <proc call> ON <selectors>"
	"UPDATE <selectors> SET var=<value>,var2=<value>"
	"DELETE <selectors>"

	"<selectors>" in this context is "<type> [IN <source>] [chain of MAP/WHERE modifiers]"

	"IN" (or "FROM", that works too but it's kinda weird to read),
	is the list of objects to work on. This defaults to world if not provided.
	But doing something like "IN living_mob_list" is quite handy and can optimize your query.
	All names inside the IN block are global scope, so you can do living_mob_list (a global var) easily.
	You can also run it on a single object. Because SDQL is that convenient even for single operations.

	<type> filters out objects of, well, that type easily. "*" is a wildcard and just takes everything in
	the source list.

	And then there's the MAP/WHERE chain.
	These operate on each individual object being ran through the query.
	They're both expressions like IN, but unlike it the expression is scoped *on the object*.
	So if you do "WHERE z == 4", this does "src.z", effectively.
	If you want to access global variables, you can do `global.living_mob_list`.
	Same goes for procs.

	MAP "changes" the object into the result of the expression.
	WHERE "drops" the object if the expression is falsey (0, null or "")

	What can you do inside expressions?

	* Proc calls
	* Variable reads
	* Literals (numbers, strings, type paths, etc...)
	* \ref referencing: {0x30000cc} grabs the object with \ref [0x30000cc]
	* Lists: [a, b, c] or [a: b, c: d]
	* Math and stuff.
	* A few special variables: src (the object currently scoped on), usr (your mob),
		marked (your marked datum), global(global scope)

	TG ADDITIONS START:
	Add USING keyword to the front of the query to use options system
	The defaults aren't necessarily implemented, as there is no need to.
	Available options: (D) means default
	PROCCALL = (D)ASYNC, BLOCKING
	SELECT = FORCE_NULLS, (D)SKIP_NULLS
	PRIORITY = HIGH, (D) NORMAL
	AUTOGC = (D) AUTOGC, KEEP_ALIVE
	SEQUENTIAL = TRUE - The queries in this batch will be executed sequentially one by one not in parallel

	Example: USING PROCCALL = BLOCKING, SELECT = FORCE_NULLS, PRIORITY = HIGH SELECT /mob FROM world WHERE z == 1

*/

#define SDQL2_STATE_ERROR 0
#define SDQL2_STATE_IDLE 1
#define SDQL2_STATE_PRESEARCH 2
#define SDQL2_STATE_SEARCHING 3
#define SDQL2_STATE_EXECUTING 4
#define SDQL2_STATE_SWITCHING 5
#define SDQL2_STATE_HALTING 6

#define SDQL2_VALID_OPTION_TYPES list("proccall", "select", "priority", "autogc" , "sequential")
#define SDQL2_VALID_OPTION_VALUES list("async", "blocking", "force_nulls", "skip_nulls", "high", "normal", "keep_alive" , "true")

#define SDQL2_OPTION_SELECT_OUTPUT_SKIP_NULLS (1<<0)
#define SDQL2_OPTION_BLOCKING_CALLS (1<<1)
#define SDQL2_OPTION_HIGH_PRIORITY (1<<2) //High priority SDQL query, allow using almost all of the tick.
#define SDQL2_OPTION_DO_NOT_AUTOGC (1<<3)
#define SDQL2_OPTION_SEQUENTIAL (1<<4)

#define SDQL2_OPTIONS_DEFAULT (SDQL2_OPTION_SELECT_OUTPUT_SKIP_NULLS)

#define SDQL2_IS_RUNNING (state == SDQL2_STATE_EXECUTING || state == SDQL2_STATE_SEARCHING || state == SDQL2_STATE_SWITCHING || state == SDQL2_STATE_PRESEARCH)
#define SDQL2_HALT_CHECK if(!SDQL2_IS_RUNNING) {state = SDQL2_STATE_HALTING; return FALSE;};

#define SDQL2_TICK_CHECK ((options & SDQL2_OPTION_HIGH_PRIORITY)? CHECK_TICK_HIGH_PRIORITY : CHECK_TICK)

#define SDQL2_STAGE_SWITCH_CHECK if(state != SDQL2_STATE_SWITCHING){\
		if(state == SDQL2_STATE_HALTING){\
			state = SDQL2_STATE_IDLE;\
			return FALSE}\
		state = SDQL2_STATE_ERROR;\
		CRASH("SDQL2 fatal error");};

/client/proc/SDQL2_query(query_text as message)
	set category = "Debug"
	if(!check_rights(R_DEBUG))  //Shouldn't happen... but just to be safe.
		message_admins(span_danger("ERROR: Non-admin [key_name(usr)] attempted to execute a SDQL query!"))
		log_admin("Non-admin [key_name(usr)] attempted to execute a SDQL query!")
		return FALSE
	var/list/results = world.SDQL2_query(query_text, key_name_admin(usr), "[key_name(usr)]")
	if(length(results) == 3)
		for(var/I in 1 to 3)
			to_chat(usr, results[I], confidential = TRUE)
	SSblackbox.record_feedback("nested tally", "SDQL query", 1, list(ckey, query_text))

/world/proc/SDQL2_query(query_text, log_entry1, log_entry2, silent = FALSE)
	var/query_log = "executed SDQL query(s): \"[query_text]\"."
	if(!silent)
		message_admins("[log_entry1] [query_log]")
	query_log = "[log_entry2] [query_log]"
	log_game(query_log)
	NOTICE(query_log)

	var/start_time_total = REALTIMEOFDAY
	var/sequential = FALSE

	if(!length(query_text))
		return
	var/list/query_list = SDQL2_tokenize(query_text)
	if(!length(query_list))
		return
	var/list/querys = SDQL_parse(query_list)
	if(!length(querys))
		return
	var/list/datum/sdql2_query/running = list()
	var/list/datum/sdql2_query/waiting_queue = list() //Sequential queries queue.

	for(var/list/query_tree in querys)
		var/datum/sdql2_query/query = new /datum/sdql2_query(query_tree)
		if(QDELETED(query))
			continue
		if(usr)
			query.show_next_to_key = usr.ckey
		waiting_queue += query
		if(query.options & SDQL2_OPTION_SEQUENTIAL)
			sequential = TRUE

	if(sequential) //Start first one
		var/datum/sdql2_query/query = popleft(waiting_queue)
		running += query
		var/msg = "Starting query #[query.id] - [query.get_query_text()]."
		if(usr)
			to_chat(usr, span_admin("[msg]"), confidential = TRUE)
		log_admin(msg)
		query.ARun()
	else //Start all
		for(var/datum/sdql2_query/query in waiting_queue)
			running += query
			var/msg = "Starting query #[query.id] - [query.get_query_text()]."
			if(usr)
				to_chat(usr, span_admin("[msg]"), confidential = TRUE)
			log_admin(msg)
			query.ARun()

	var/finished = FALSE
	var/objs_all = 0
	var/objs_eligible = 0
	var/selectors_used = FALSE
	var/list/combined_refs = list()
	do
		CHECK_TICK
		finished = TRUE
		for(var/i in running)
			var/datum/sdql2_query/query = i
			if(QDELETED(query))
				running -= query
				continue
			else if(query.state != SDQL2_STATE_IDLE)
				finished = FALSE
				if(query.state == SDQL2_STATE_ERROR)
					if(usr)
						to_chat(usr, span_admin("SDQL query [query.get_query_text()] errored. It will NOT be automatically garbage collected. Please remove manually."), confidential = TRUE)
					running -= query
			else
				if(query.finished)
					objs_all += islist(query.obj_count_all)? length(query.obj_count_all) : query.obj_count_all
					objs_eligible += islist(query.obj_count_eligible)? length(query.obj_count_eligible) : query.obj_count_eligible
					selectors_used |= query.where_switched
					combined_refs |= query.select_refs
					running -= query
					if(!(query.options & SDQL2_OPTION_DO_NOT_AUTOGC))
						QDEL_IN(query, 50)
					if(sequential && waiting_queue.len)
						finished = FALSE
						var/datum/sdql2_query/next_query = popleft(waiting_queue)
						running += next_query
						var/msg = "Starting query #[next_query.id] - [next_query.get_query_text()]."
						if(usr)
							to_chat(usr, span_admin("[msg]"), confidential = TRUE)
						log_admin(msg)
						next_query.ARun()
				else
					if(usr)
						to_chat(usr, span_admin("SDQL query [query.get_query_text()] was halted. It will NOT be automatically garbage collected. Please remove manually."), confidential = TRUE)
					running -= query
	while(!finished)

	var/end_time_total = REALTIMEOFDAY - start_time_total
	return list(span_admin("SDQL query combined results: [query_text]"),\
		span_admin("SDQL query completed: [objs_all] objects selected by path, and [selectors_used ? objs_eligible : objs_all] objects executed on after WHERE filtering/MAPping if applicable."),\
		span_admin("SDQL combined querys took [DisplayTimeText(end_time_total)] to complete.")) + combined_refs

GLOBAL_LIST_INIT(sdql2_queries, GLOB.sdql2_queries || list())
GLOBAL_DATUM_INIT(sdql2_vv_statobj, /obj/effect/statclick/sdql2_vv_all, new(null, "VIEW VARIABLES (all)", null))

/datum/sdql2_query
	var/list/query_tree
	var/state = SDQL2_STATE_IDLE
	var/options = SDQL2_OPTIONS_DEFAULT
	var/superuser = FALSE //Run things like proccalls without using admin protections
	var/allow_admin_interact = TRUE //Allow admins to do things to this excluding varedit these two vars
	var/static/id_assign = 1
	var/id = 0

	var/qdel_on_finish = FALSE

	//Last run
		//General
	var/finished = FALSE
	var/start_time
	var/end_time
	var/where_switched = FALSE
	var/show_next_to_key
		//Select query only
	var/list/select_refs
	var/list/select_text
		//Runtime tracked
			//These three are weird. For best performance, they are only a number when they're not being changed by the SDQL searching/execution code. They only become numbers when they finish changing.
	var/list/obj_count_all
	var/list/obj_count_eligible
	var/obj_count_finished

	//Statclick
	var/obj/effect/statclick/SDQL2_delete/delete_click
	var/obj/effect/statclick/SDQL2_action/action_click

/datum/sdql2_query/New(list/tree, SU = FALSE, admin_interact = TRUE, _options = SDQL2_OPTIONS_DEFAULT, finished_qdel = FALSE)
	if(IsAdminAdvancedProcCall() || !LAZYLEN(tree))
		qdel(src)
		return
	LAZYADD(GLOB.sdql2_queries, src)
	superuser = SU
	allow_admin_interact = admin_interact
	query_tree = tree
	options = _options
	id = id_assign++
	qdel_on_finish = finished_qdel

/datum/sdql2_query/Destroy()
	state = SDQL2_STATE_HALTING
	query_tree = null
	obj_count_all = null
	obj_count_eligible = null
	obj_count_finished = null
	select_text = null
	select_refs = null
	GLOB.sdql2_queries -= src
	return ..()

/datum/sdql2_query/proc/get_query_text()
	var/list/out = list()
	recursive_list_print(out, query_tree)
	return out.Join()

/proc/recursive_list_print(list/output = list(), list/input, datum/callback/datum_handler, datum/callback/atom_handler)
	output += "\[ "
	for(var/i in 1 to input.len)
		var/final = i == input.len
		var/key = input[i]

		//print the key
		if(islist(key))
			recursive_list_print(output, key, datum_handler, atom_handler)
		else if(is_proper_datum(key) && (datum_handler || (isatom(key) && atom_handler)))
			if(isatom(key) && atom_handler)
				output += atom_handler.Invoke(key)
			else
				output += datum_handler.Invoke(key)
		else
			output += "[key]"

		//print the value
		var/is_value = (!isnum(key) && !isnull(input[key]))
		if(is_value)
			var/value = input[key]
			if(islist(value))
				recursive_list_print(output, value, datum_handler, atom_handler)
			else if(is_proper_datum(value) && (datum_handler || (isatom(value) && atom_handler)))
				if(isatom(value) && atom_handler)
					output += atom_handler.Invoke(value)
				else
					output += datum_handler.Invoke(value)
			else
				output += " = [value]"

		if(!final)
			output += " , "

	output += " \]"

/datum/sdql2_query/proc/text_state()
	switch(state)
		if(SDQL2_STATE_ERROR)
			return "###ERROR"
		if(SDQL2_STATE_IDLE)
			return "####IDLE"
		if(SDQL2_STATE_PRESEARCH)
			return "PRESEARCH"
		if(SDQL2_STATE_SEARCHING)
			return "SEARCHING"
		if(SDQL2_STATE_EXECUTING)
			return "EXECUTING"
		if(SDQL2_STATE_SWITCHING)
			return "SWITCHING"
		if(SDQL2_STATE_HALTING)
			return "##HALTING"

/datum/sdql2_query/proc/generate_stat()
	if(!allow_admin_interact)
		return
	if(!delete_click)
		delete_click = new(null, "INITIALIZING", src)
	if(!action_click)
		action_click = new(null, "INITIALIZNG", src)
	var/list/L = list()
	L[++L.len] = list("[id] ", "[delete_click.update("DELETE QUERY | STATE : [text_state()] | ALL/ELIG/FIN \
	[islist(obj_count_all)? length(obj_count_all) : (isnull(obj_count_all)? "0" : obj_count_all)]/\
	[islist(obj_count_eligible)? length(obj_count_eligible) : (isnull(obj_count_eligible)? "0" : obj_count_eligible)]/\
	[islist(obj_count_finished)? length(obj_count_finished) : (isnull(obj_count_finished)? "0" : obj_count_finished)] - [get_query_text()]")]", REF(delete_click))
	L[++L.len] = list(" ", "[action_click.update("[SDQL2_IS_RUNNING? "HALT" : "RUN"]")]", REF(action_click))
	return L

/datum/sdql2_query/proc/delete_click()
	admin_del(usr)

/datum/sdql2_query/proc/action_click()
	if(SDQL2_IS_RUNNING)
		admin_halt(usr)
	else
		admin_run(usr)

/datum/sdql2_query/proc/admin_halt(user = usr)
	if(!SDQL2_IS_RUNNING)
		return
	var/msg = "[key_name(user)] has halted query #[id]"
	message_admins(msg)
	log_admin(msg)
	state = SDQL2_STATE_HALTING

/datum/sdql2_query/proc/admin_run(mob/user = usr)
	if(SDQL2_IS_RUNNING)
		return
	var/msg = "[key_name(user)] has (re)started query #[id]"
	message_admins(msg)
	log_admin(msg)
	show_next_to_key = user.ckey
	ARun()

/datum/sdql2_query/proc/admin_del(user = usr)
	var/msg = "[key_name(user)] has stopped + deleted query #[id]"
	message_admins(msg)
	log_admin(msg)
	qdel(src)

/datum/sdql2_query/proc/set_option(name, value)
	switch(name)
		if("select")
			switch(value)
				if("force_nulls")
					options &= ~(SDQL2_OPTION_SELECT_OUTPUT_SKIP_NULLS)
		if("proccall")
			switch(value)
				if("blocking")
					options |= SDQL2_OPTION_BLOCKING_CALLS
		if("priority")
			switch(value)
				if("high")
					options |= SDQL2_OPTION_HIGH_PRIORITY
		if("autogc")
			switch(value)
				if("keep_alive")
					options |= SDQL2_OPTION_DO_NOT_AUTOGC
		if("sequential")
			switch(value)
				if("true")
					options |= SDQL2_OPTION_SEQUENTIAL

/datum/sdql2_query/proc/ARun()
	INVOKE_ASYNC(src, .proc/Run)

/datum/sdql2_query/proc/Run()
	if(SDQL2_IS_RUNNING)
		return FALSE
	if(query_tree["options"])
		for(var/name in query_tree["options"])
			var/value = query_tree["options"][name]
			set_option(name, value)
	select_refs = list()
	select_text = null
	obj_count_all = 0
	obj_count_eligible = 0
	obj_count_finished = 0
	start_time = REALTIMEOFDAY

	state = SDQL2_STATE_PRESEARCH
	var/list/search_tree = PreSearch()
	SDQL2_STAGE_SWITCH_CHECK

	state = SDQL2_STATE_SEARCHING
	var/list/found = Search(search_tree)
	SDQL2_STAGE_SWITCH_CHECK

	state = SDQL2_STATE_EXECUTING
	Execute(found)
	SDQL2_STAGE_SWITCH_CHECK

	end_time = REALTIMEOFDAY
	state = SDQL2_STATE_IDLE
	finished = TRUE
	. = TRUE
	if(show_next_to_key)
		var/client/C = GLOB.directory[show_next_to_key]
		if(C)
			var/mob/showmob = C.mob
			to_chat(showmob, "SDQL query results: [get_query_text()]
\
			SDQL query completed: [islist(obj_count_all)? length(obj_count_all) : obj_count_all] objects selected by path, and \
			[where_switched? "[islist(obj_count_eligible)? length(obj_count_eligible) : obj_count_eligible] objects executed on after WHERE keyword selection." : ""]
\
			SDQL query took [DisplayTimeText(end_time - start_time)] to complete.", confidential = TRUE)
			if(length(select_text))
				var/text = islist(select_text)? select_text.Join() : select_text
				var/static/result_offset = 0
				showmob << browse(text, "window=SDQL-result-[result_offset++]")
	show_next_to_key = null
	if(qdel_on_finish)
		qdel(src)

/datum/sdql2_query/proc/PreSearch()
	SDQL2_HALT_CHECK
	switch(query_tree[1])
		if("explain")
			SDQL_testout(query_tree["explain"])
			state = SDQL2_STATE_HALTING
			return
		if("call")
			. = query_tree["on"]
		if("select", "delete", "update")
			. = query_tree[query_tree[1]]
	state = SDQL2_STATE_SWITCHING

/datum/sdql2_query/proc/Search(list/tree)
	SDQL2_HALT_CHECK
	var/type = tree[1]
	var/list/from = tree[2]
	var/list/objs = SDQL_from_objs(from)
	SDQL2_TICK_CHECK
	SDQL2_HALT_CHECK
	objs = SDQL_get_all(type, objs)
	SDQL2_TICK_CHECK
	SDQL2_HALT_CHECK

	// 1 and 2 are type and FROM.
	var/i = 3
	while (i <= tree.len)
		var/key = tree[i++]
		var/list/expression = tree[i++]
		switch (key)
			if ("map")
				for(var/j = 1 to objs.len)
					var/x = objs[j]
					objs[j] = SDQL_expression(x, expression)
					SDQL2_TICK_CHECK
					SDQL2_HALT_CHECK

			if ("where")
				where_switched = TRUE
				var/list/out = list()
				obj_count_eligible = out
				for(var/x in objs)
					if(SDQL_expression(x, expression))
						out += x
					SDQL2_TICK_CHECK
					SDQL2_HALT_CHECK
				objs = out
	if(islist(obj_count_eligible))
		obj_count_eligible = objs.len
	else
		obj_count_eligible = obj_count_all
	. = objs
	state = SDQL2_STATE_SWITCHING

/datum/sdql2_query/proc/SDQL_from_objs(list/tree)
	if(IsAdminAdvancedProcCall())
		if("world" in tree)
			var/text = "[key_name(usr)] attempted to grab world with a procedure call to a SDQL datum."
			message_admins(text)
			log_admin(text)
			return
	if("world" in tree)
		return world
	return SDQL_expression(world, tree)

/datum/sdql2_query/proc/SDQL_get_all(type, location)
	var/list/out = list()
	obj_count_all = out

// If only a single object got returned, wrap it into a list so the for loops run on it.
	if(!islist(location) && location != world)
		location = list(location)

	if(type == "*")
		for(var/i in location)
			var/datum/d = i
			if(d.can_vv_get() || superuser)
				out += d
			SDQL2_TICK_CHECK
			SDQL2_HALT_CHECK
		return out
	if(istext(type))
		type = text2path(type)
	var/typecache = typecacheof(type)

	if(ispath(type, /mob))
		for(var/mob/d in location)
			if(typecache[d.type] && (d.can_vv_get() || superuser))
				out += d
			SDQL2_TICK_CHECK
			SDQL2_HALT_CHECK

	else if(ispath(type, /turf))
		for(var/turf/d in location)
			if(typecache[d.type] && (d.can_vv_get() || superuser))
				out += d
			SDQL2_TICK_CHECK
			SDQL2_HALT_CHECK

	else if(ispath(type, /obj))
		for(var/obj/d in location)
			if(typecache[d.type] && (d.can_vv_get() || superuser))
				out += d
			SDQL2_TICK_CHECK
			SDQL2_HALT_CHECK

	else if(ispath(type, /area))
		for(var/area/d in location)
			if(typecache[d.type] && (d.can_vv_get() || superuser))
				out += d
			SDQL2_TICK_CHECK
			SDQL2_HALT_CHECK

	else if(ispath(type, /atom))
		for(var/atom/d in location)
			if(typecache[d.type] && (d.can_vv_get() || superuser))
				out += d
			SDQL2_TICK_CHECK
			SDQL2_HALT_CHECK

	else if(ispath(type, /datum))
		if(location == world) //snowflake for byond shortcut
			for(var/datum/d) //stupid byond trick to have it not return atoms to make this less laggy
				if(typecache[d.type] && (d.can_vv_get() || superuser))
					out += d
				SDQL2_TICK_CHECK
				SDQL2_HALT_CHECK
		else
			for(var/datum/d in location)
				if(typecache[d.type] && (d.can_vv_get() || superuser))
					out += d
				SDQL2_TICK_CHECK
				SDQL2_HALT_CHECK
	obj_count_all = out.len
	return out

/datum/sdql2_query/proc/Execute(list/found)
	SDQL2_HALT_CHECK
	select_refs = list()
	select_text = list()
	switch(query_tree[1])
		if("call")
			for(var/i in found)
				if(!is_proper_datum(i))
					continue
				world.SDQL_var(i, query_tree["call"][1], null, i, superuser, src)
				obj_count_finished++
				SDQL2_TICK_CHECK
				SDQL2_HALT_CHECK

		if("delete")
			for(var/datum/d in found)
				qdel(d)
				obj_count_finished++
				SDQL2_TICK_CHECK
				SDQL2_HALT_CHECK

		if("select")
			var/list/text_list = list()
			var/print_nulls = !(options & SDQL2_OPTION_SELECT_OUTPUT_SKIP_NULLS)
			obj_count_finished = select_refs
			for(var/i in found)
				SDQL_print(i, text_list, print_nulls)
				select_refs[REF(i)] = TRUE
				SDQL2_TICK_CHECK
				SDQL2_HALT_CHECK
			select_text = text_list

		if("update")
			if("set" in query_tree)
				var/list/set_list = query_tree["set"]
				for(var/d in found)
					if(!is_proper_datum(d))
						continue
					SDQL_internal_vv(d, set_list)
					obj_count_finished++
					SDQL2_TICK_CHECK
					SDQL2_HALT_CHECK
	if(islist(obj_count_finished))
		obj_count_finished = length(obj_count_finished)
	state = SDQL2_STATE_SWITCHING

/datum/sdql2_query/proc/SDQL_print(object, list/text_list, print_nulls = TRUE)
	if(is_proper_datum(object))
		text_list += "[REF(object)] : [object]"
		if(istype(object, /atom))
			var/atom/A = object
			var/turf/T = A.loc
			var/area/a
			if(istype(T))
				text_list += " at [T] [ADMIN_COORDJMP(T)]"
				a = T.loc
			else
				var/turf/final = get_turf(T) //Recursive, hopefully?
				if(istype(final))
					text_list += " at [final] [ADMIN_COORDJMP(final)]"
					a = final.loc
				else
					text_list += " at nonexistent location"
			if(a)
				text_list += " in area [a]"
				if(T.loc != a)
					text_list += " inside [T]"
		text_list += "
"
	else if(islist(object))
		var/list/L = object
		var/first = TRUE
		text_list += "\["
		for (var/x in L)
			if (!first)
				text_list += ", "
			first = FALSE
			SDQL_print(x, text_list)
			if (!isnull(x) && !isnum(x) && L[x] != null)
				text_list += " -> "
				SDQL_print(L[L[x]])
		text_list += "]
"
	else
		if(isnull(object))
			if(print_nulls)
				text_list += "NULL
"
		else
			text_list += "[object]
"

/datum/sdql2_query/CanProcCall()
	if(!allow_admin_interact)
		return FALSE
	return ..()

/datum/sdql2_query/vv_edit_var(var_name, var_value)
	if(!allow_admin_interact)
		return FALSE
	if(var_name == NAMEOF(src, superuser) || var_name == NAMEOF(src, allow_admin_interact) || var_name == NAMEOF(src, query_tree))
		return FALSE
	return ..()

/datum/sdql2_query/proc/SDQL_internal_vv(d, list/set_list)
	for(var/list/sets in set_list)
		var/datum/temp = d
		var/i = 0
		for(var/v in sets)
			if(v == "#null")
				SDQL_expression(d, set_list[sets])
				break
			if(++i == sets.len)
				if(superuser)
					if(temp.vars.Find(v))
						temp.vars[v] = SDQL_expression(d, set_list[sets])
				else
					temp.vv_edit_var(v, SDQL_expression(d, set_list[sets]))
				break
			if(temp.vars.Find(v) && (istype(temp.vars[v], /datum) || istype(temp.vars[v], /client)))
				temp = temp.vars[v]
			else
				break

/datum/sdql2_query/proc/SDQL_function_blocking(datum/object, procname, list/arguments, source)
	var/list/new_args = list()
	for(var/arg in arguments)
		new_args[++new_args.len] = SDQL_expression(source, arg)
	if(object == GLOB) // Global proc.
		return superuser ? (call("/proc/[procname]")(arglist(new_args))) : (WrapAdminProcCall(GLOBAL_PROC, procname, new_args))
	return superuser ? (call(object, procname)(arglist(new_args))) : (WrapAdminProcCall(object, procname, new_args))

/datum/sdql2_query/proc/SDQL_function_async(datum/object, procname, list/arguments, source)
	set waitfor = FALSE
	return SDQL_function_blocking(object, procname, arguments, source)

/datum/sdql2_query/proc/SDQL_expression(datum/object, list/expression, start = 1)
	var/result = 0
	var/val

	for(var/i = start, i <= expression.len, i++)
		var/op = ""

		if(i > start)
			op = expression[i]
			i++

		var/list/ret = SDQL_value(object, expression, i)
		val = ret["val"]
		i = ret["i"]

		if(op != "")
			switch(op)
				if("+")
					result = (result + val)
				if("-")
					result = (result - val)
				if("*")
					result = (result * val)
				if("/")
					result = (result / val)
				if("&")
					result = (result & val)
				if("|")
					result = (result | val)
				if("^")
					result = (result ^ val)
				if("%")
					result = (result % val)
				if("=", "==")
					result = (result == val)
				if("!=", "<>")
					result = (result != val)
				if("<")
					result = (result < val)
				if("<=")
					result = (result <= val)
				if(">")
					result = (result > val)
				if(">=")
					result = (result >= val)
				if("and", "&&")
					result = (result && val)
				if("or", "||")
					result = (result || val)
				else
					to_chat(usr, span_danger("SDQL2: Unknown op [op]"), confidential = TRUE)
					result = null
		else
			result = val

	return result

/datum/sdql2_query/proc/SDQL_value(datum/object, list/expression, start = 1)
	var/i = start
	var/val = null

	if(i > expression.len)
		return list("val" = null, "i" = i)

	if(istype(expression[i], /list))
		val = SDQL_expression(object, expression[i])

	else if(expression[i] == "TRUE")
		val = TRUE

	else if(expression[i] == "FALSE")
		val = FALSE

	else if(expression[i] == "!")
		var/list/ret = SDQL_value(object, expression, i + 1)
		val = !ret["val"]
		i = ret["i"]

	else if(expression[i] == "~")
		var/list/ret = SDQL_value(object, expression, i + 1)
		val = ~ret["val"]
		i = ret["i"]

	else if(expression[i] == "-")
		var/list/ret = SDQL_value(object, expression, i + 1)
		val = -ret["val"]
		i = ret["i"]

	else if(expression[i] == "null")
		val = null

	else if(isnum(expression[i]))
		val = expression[i]

	else if(ispath(expression[i]))
		val = expression[i]

	else if(expression[i][1] in list("'", "\""))
		val = copytext_char(expression[i], 2, -1)

	else if(expression[i] == "\[")
		var/list/expressions_list = expression[++i]
		val = list()
		for(var/list/expression_list in expressions_list)
			var/result = SDQL_expression(object, expression_list)
			var/assoc
			if(expressions_list[expression_list] != null)
				assoc = SDQL_expression(object, expressions_list[expression_list])
			if(assoc != null)
				// Need to insert the key like this to prevent duplicate keys fucking up.
				var/list/dummy = list()
				dummy[result] = assoc
				result = dummy
			val += result

	else if(expression[i] == "@\[")
		var/list/search_tree = expression[++i]
		var/already_searching = (state == SDQL2_STATE_SEARCHING) //In case we nest, don't want to break out of the searching state until we're all done.

		if(!already_searching)
			state = SDQL2_STATE_SEARCHING

		val = Search(search_tree)
		SDQL2_STAGE_SWITCH_CHECK

		if(!already_searching)
			state = SDQL2_STATE_EXECUTING
		else
			state = SDQL2_STATE_SEARCHING

	else
		val = world.SDQL_var(object, expression, i, object, superuser, src)
		i = expression.len

	return list("val" = val, "i" = i)

/proc/SDQL_parse(list/query_list)
	var/datum/sdql_parser/parser = new()
	var/list/querys = list()
	var/list/query_tree = list()
	var/pos = 1
	var/querys_pos = 1
	var/do_parse = 0

	for(var/val in query_list)
		if(val == ";")
			do_parse = 1
		else if(pos >= query_list.len)
			query_tree += val
			do_parse = 1

		if(do_parse)
			parser.query = query_tree
			var/list/parsed_tree
			parsed_tree = parser.parse()
			if(parsed_tree.len > 0)
				querys.len = querys_pos
				querys[querys_pos] = parsed_tree
				querys_pos++
			else //There was an error so don't run anything, and tell the user which query has errored.
				to_chat(usr, span_danger("Parsing error on [querys_pos]\th query. Nothing was executed."), confidential = TRUE)
				return list()
			query_tree = list()
			do_parse = 0
		else
			query_tree += val
		pos++

	qdel(parser)
	return querys

/proc/SDQL_testout(list/query_tree, indent = 0)
	var/static/whitespace = "    "
	var/spaces = ""
	for(var/s = 0, s < indent, s++)
		spaces += whitespace

	for(var/item in query_tree)
		if(istype(item, /list))
			to_chat(usr, "[spaces](", confidential = TRUE)
			SDQL_testout(item, indent + 1)
			to_chat(usr, "[spaces])", confidential = TRUE)

		else
			to_chat(usr, "[spaces][item]", confidential = TRUE)

		if(!isnum(item) && query_tree[item])

			if(istype(query_tree[item], /list))
				to_chat(usr, "[spaces][whitespace](", confidential = TRUE)
				SDQL_testout(query_tree[item], indent + 2)
				to_chat(usr, "[spaces][whitespace])", confidential = TRUE)

			else
				to_chat(usr, "[spaces][whitespace][query_tree[item]]", confidential = TRUE)

//Staying as a world proc as this is called too often for changes to offset the potential IsAdminAdvancedProcCall checking overhead.
/world/proc/SDQL_var(object, list/expression, start = 1, source, superuser, datum/sdql2_query/query)
	var/v
	var/static/list/exclude = list("usr", "src", "marked", "global", "MC", "FS", "CFG")
	var/long = start < expression.len
	var/datum/D
	if(is_proper_datum(object))
		D = object

	if (object == world && (!long || expression[start + 1] == ".") && !(expression[start] in exclude) && copytext(expression[start], 1, 3) != "SS") //3 == length("SS") + 1
		to_chat(usr, span_danger("World variables are not allowed to be accessed. Use global."), confidential = TRUE)
		return null

	else if(expression [start] == "{" && long)
		if(lowertext(copytext(expression[start + 1], 1, 3)) != "0x") //3 == length("0x") + 1
			to_chat(usr, span_danger("Invalid pointer syntax: [expression[start + 1]]"), confidential = TRUE)
			return null
		v = locate("\[[expression[start + 1]]]")
		if(!v)
			to_chat(usr, span_danger("Invalid pointer: [expression[start + 1]]"), confidential = TRUE)
			return null
		start++
		long = start < expression.len
	else if(expression[start] == "(" && long)
		v = query.SDQL_expression(source, expression[start + 1])
		start++
		long = start < expression.len
	else if(D != null && (!long || expression[start + 1] == ".") && (expression[start] in D.vars))
		if(D.can_vv_get(expression[start]) || superuser)
			v = D.vars[expression[start]]
		else
			v = "SECRET"
	else if(D != null && long && expression[start + 1] == ":" && hascall(D, expression[start]))
		v = expression[start]
	else if(!long || expression[start + 1] == ".")
		switch(expression[start])
			if("usr")
				v = usr
			if("src")
				v = source
			if("marked")
				if(usr.client && usr.client.holder && usr.client.holder.marked_datum)
					v = usr.client.holder.marked_datum
				else
					return null
			if("world")
				v = world
			if("global")
				v = GLOB
			if("MC")
				v = Master
			if("FS")
				v = Failsafe
			if("CFG")
				v = config
			else
				if(copytext(expression[start], 1, 3) == "SS") //Subsystem //3 == length("SS") + 1
					var/SSname = copytext_char(expression[start], 3)
					var/SSlength = length(SSname)
					var/datum/controller/subsystem/SS
					var/SSmatch
					for(var/_SS in Master.subsystems)
						SS = _SS
						if(copytext("[SS.type]", -SSlength) == SSname)
							SSmatch = SS
							break
					if(!SSmatch)
						return null
					v = SSmatch
				else
					return null
	else if(object == GLOB) // Shitty ass hack kill me.
		v = expression[start]
	if(long)
		if(expression[start + 1] == ".")
			return SDQL_var(v, expression[start + 2], null, source, superuser, query)
		else if(expression[start + 1] == ":")
			return (query.options & SDQL2_OPTION_BLOCKING_CALLS)? query.SDQL_function_async(object, v, expression[start + 2], source) : query.SDQL_function_blocking(object, v, expression[start + 2], source)
		else if(expression[start + 1] == "\[" && islist(v))
			var/list/L = v
			var/index = query.SDQL_expression(source, expression[start + 2])
			if(isnum(index) && (!ISINTEGER(index) || L.len < index))
				to_chat(usr, span_danger("Invalid list index: [index]"), confidential = TRUE)
				return null
			return L[index]
	return v

/proc/SDQL2_tokenize(query_text)

	var/list/whitespace = list(" ", "\n", "\t")
	var/list/single = list("(", ")", ",", "+", "-", ".", "\[", "]", "{", "}", ";", ":")
	var/list/multi = list(
					"=" = list("", "="),
					"<" = list("", "=", ">"),
					">" = list("", "="),
					"!" = list("", "="),
					"@" = list("\["))

	var/word = ""
	var/list/query_list = list()
	var/len = length(query_text)
	var/char = ""

	for(var/i = 1, i <= len, i += length(char))
		char = query_text[i]

		if(char in whitespace)
			if(word != "")
				query_list += word
				word = ""

		else if(char in single)
			if(word != "")
				query_list += word
				word = ""

			query_list += char

		else if(char in multi)
			if(word != "")
				query_list += word
				word = ""

			var/char2 = query_text[i + length(char)]

			if(char2 in multi[char])
				query_list += "[char][char2]"
				i++

			else
				query_list += char

		else if(char == "'")
			if(word != "")
				to_chat(usr, "\red SDQL2: You have an error in your SDQL syntax, unexpected ' in query: \"[query_text]\" following \"[word]\". Please check your syntax, and try again.", confidential = TRUE)
				return null

			word = "'"

			for(i += length(char), i <= len, i += length(char))
				char = query_text[i]

				if(char == "'")
					if(query_text[i + length(char)] == "'")
						word += "'"
						i += length(query_text[i + length(char)])

					else
						break

				else
					word += char

			if(i > len)
				to_chat(usr, "\red SDQL2: You have an error in your SDQL syntax, unmatched ' in query: \"[query_text]\". Please check your syntax, and try again.", confidential = TRUE)
				return null

			query_list += "[word]'"
			word = ""

		else if(char == "\"")
			if(word != "")
				to_chat(usr, "\red SDQL2: You have an error in your SDQL syntax, unexpected \" in query: \"[query_text]\" following \"[word]\". Please check your syntax, and try again.", confidential = TRUE)
				return null

			word = "\""

			for(i += length(char), i <= len, i += length(char))
				char = query_text[i]

				if(char == "\"")
					if((i + length(char) <= len) && query_text[i + length(char)] == "'")
						word += "\""
						i += length(query_text[i + length(char)])

					else
						break

				else
					word += char

			if(i > len)
				to_chat(usr, "\red SDQL2: You have an error in your SDQL syntax, unmatched \" in query: \"[query_text]\". Please check your syntax, and try again.", confidential = TRUE)
				return null

			query_list += "[word]\""
			word = ""

		else
			word += char

	if(word != "")
		query_list += word
	return query_list

/proc/is_proper_datum(thing)
	return istype(thing, /datum) || istype(thing, /client)

/obj/effect/statclick/SDQL2_delete/Click()
	if(!usr.client?.holder)
		message_admins("[key_name_admin(usr)] non-holder clicked on a statclick! ([src])")
		log_game("[key_name(usr)] non-holder clicked on a statclick! ([src])")
		return
	var/datum/sdql2_query/Q = target
	Q.delete_click()

/obj/effect/statclick/SDQL2_action/Click()
	if(!usr.client?.holder)
		message_admins("[key_name_admin(usr)] non-holder clicked on a statclick! ([src])")
		log_game("[key_name(usr)] non-holder clicked on a statclick! ([src])")
		return
	var/datum/sdql2_query/Q = target
	Q.action_click()

/obj/effect/statclick/sdql2_vv_all
	name = "VIEW VARIABLES"

/obj/effect/statclick/sdql2_vv_all/Click()
	if(!usr.client?.holder)
		message_admins("[key_name_admin(usr)] non-holder clicked on a statclick! ([src])")
		log_game("[key_name(usr)] non-holder clicked on a statclick! ([src])")
		return
	usr.client.debug_variables(GLOB.sdql2_queries)


code:
//I'm pretty sure that this is a recursive [s]descent[/s] ascent parser.

//Spec

//////////
//
// query : select_query | delete_query | update_query | call_query | explain
// explain : 'EXPLAIN' query
// select_query : 'SELECT' object_selectors
// delete_query : 'DELETE' object_selectors
// update_query : 'UPDATE' object_selectors 'SET' assignments
// call_query : 'CALL' variable 'ON' object_selectors // Note here: 'variable' does function calls. This simplifies parsing.
//
// select_item : '*' | object_type
//
//  object_selectors    :   select_item [('FROM' | 'IN') from_item] [modifier_list]
//  modifier_list       :   ('WHERE' bool_expression | 'MAP' expression) [modifier_list]
//
// from_item : 'world' | expression
//
// call_function : <function name> '(' [expression_list] ')'
//
// object_type : <type path>
//
// assignments : assignment [',' assignments]
// assignment : <variable name> '=' expression
// variable : <variable name> | variable '.' variable | variable '[' <list index> ']' | '{' <ref as hex number> '}' | '(' expression ')' | call_function
//
// bool_expression : expression comparator expression  [bool_operator bool_expression]
// expression : ( unary_expression | '(' expression ')' | value ) [binary_operator expression]
// expression_list : expression [',' expression_list]
// unary_expression : unary_operator ( unary_expression | value )
//
// comparator : '=' | '==' | '!=' | '<>' | '<' | '<=' | '>' | '>='
// value : variable | string | number | 'null' | object_type | array | selectors_array
// unary_operator : '!' | '-' | '~'
// binary_operator : comparator | '+' | '-' | '/' | '*' | '&' | '|' | '^' | '%'
// bool_operator : 'AND' | '&&' | 'OR' | '||'
//
// array : '[' expression_list ']'
// selectors_array : '@[' object_selectors ']'
//
// string : ''' <some text> ''' | '"' <some text > '"'
// number : <some digits>
//
//////////

/datum/sdql_parser
	var/query_type
	var/error = 0

	var/list/query
	var/list/tree

	var/list/boolean_operators = list("and", "or", "&&", "||")
	var/list/unary_operators = list("!", "-", "~")
	var/list/binary_operators = list("+", "-", "/", "*", "&", "|", "^", "%")
	var/list/comparators = list("=", "==", "!=", "<>", "<", "<=", ">", ">=")

/datum/sdql_parser/New(query_list)
	query = query_list

/datum/sdql_parser/proc/parse_error(error_message)
	error = 1
	to_chat(usr, span_warning("SDQL2 Parsing Error: [error_message]"), confidential = TRUE)
	return query.len + 1

/datum/sdql_parser/proc/parse()
	tree = list()
	query_options(1, tree)

	if(error)
		return list()
	else
		return tree

/datum/sdql_parser/proc/token(i)
	if(i <= query.len)
		return query[i]

	else
		return null

/datum/sdql_parser/proc/tokens(i, num)
	if(i + num <= query.len)
		return query.Copy(i, i + num)

	else
		return null

/datum/sdql_parser/proc/tokenl(i)
	return lowertext(token(i))

/datum/sdql_parser/proc/query_options(i, list/node)
	var/list/options = list()
	if(tokenl(i) == "using")
		i = option_assignments(i + 1, node, options)
	query(i, node)
	if(length(options))
		node["options"] = options

//option_assignment: query_option '=' define
/datum/sdql_parser/proc/option_assignment(i, list/node, list/assignment_list = list())
	var/type = tokenl(i)
	if(!(type in SDQL2_VALID_OPTION_TYPES))
		parse_error("Invalid option type: [type]")
	if(!(token(i + 1) == "="))
		parse_error("Invalid option assignment symbol: [token(i + 1)]")
	var/val = tokenl(i + 2)
	if(!(val in SDQL2_VALID_OPTION_VALUES))
		parse_error("Invalid option value: [val]")
	assignment_list[type] = val
	return (i + 3)

//option_assignments: option_assignment, [',' option_assignments]
/datum/sdql_parser/proc/option_assignments(i, list/node, list/store)
	i = option_assignment(i, node, store)

	if(token(i) == ",")
		i = option_assignments(i + 1, node, store)

	return i

//query: select_query | delete_query | update_query
/datum/sdql_parser/proc/query(i, list/node)
	query_type = tokenl(i)

	switch(query_type)
		if("select")
			select_query(i, node)

		if("delete")
			delete_query(i, node)

		if("update")
			update_query(i, node)

		if("call")
			call_query(i, node)

		if("explain")
			node += "explain"
			node["explain"] = list()
			query(i + 1, node["explain"])

// select_query: 'SELECT' object_selectors
/datum/sdql_parser/proc/select_query(i, list/node)
	var/list/select = list()
	i = object_selectors(i + 1, select)

	node["select"] = select
	return i

//delete_query: 'DELETE' object_selectors
/datum/sdql_parser/proc/delete_query(i, list/node)
	var/list/select = list()
	i = object_selectors(i + 1, select)

	node["delete"] = select

	return i

//update_query: 'UPDATE' object_selectors 'SET' assignments
/datum/sdql_parser/proc/update_query(i, list/node)
	var/list/select = list()
	i = object_selectors(i + 1, select)

	node["update"] = select

	if(tokenl(i) != "set")
		i = parse_error("UPDATE has misplaced SET")

	var/list/set_assignments = list()
	i = assignments(i + 1, set_assignments)

	node["set"] = set_assignments

	return i

//call_query: 'CALL' call_function ['ON' object_selectors]
/datum/sdql_parser/proc/call_query(i, list/node)
	var/list/func = list()
	i = variable(i + 1, func) // Yes technically does anything variable() matches but I don't care, if admins fuck up this badly then they shouldn't be allowed near SDQL.

	node["call"] = func

	if(tokenl(i) != "on")
		return parse_error("You need to specify what to call ON.")

	var/list/select = list()
	i = object_selectors(i + 1, select)

	node["on"] = select

	return i

// object_selectors: select_item [('FROM' | 'IN') from_item] [modifier_list]
/datum/sdql_parser/proc/object_selectors(i, list/node)
	i = select_item(i, node)

	if (tokenl(i) == "from" || tokenl(i) == "in")
		i++
		var/list/from = list()
		i = from_item(i, from)
		node[++node.len] = from

	else
		node[++node.len] = list("world")

	i = modifier_list(i, node)
	return i

// modifier_list: ('WHERE' bool_expression | 'MAP' expression) [modifier_list]
/datum/sdql_parser/proc/modifier_list(i, list/node)
	while (TRUE)
		if (tokenl(i) == "where")
			i++
			node += "where"
			var/list/expr = list()
			i = bool_expression(i, expr)
			node[++node.len] = expr

		else if (tokenl(i) == "map")
			i++
			node += "map"
			var/list/expr = list()
			i = expression(i, expr)
			node[++node.len] = expr

		else
			return i

//select_list:select_item [',' select_list]
/datum/sdql_parser/proc/select_list(i, list/node)
	i = select_item(i, node)

	if(token(i) == ",")
		i = select_list(i + 1, node)

	return i

//assignments: assignment, [',' assignments]
/datum/sdql_parser/proc/assignments(i, list/node)
	i = assignment(i, node)

	if(token(i) == ",")
		i = assignments(i + 1, node)

	return i

//select_item: '*' | select_function | object_type
/datum/sdql_parser/proc/select_item(i, list/node)
	if (token(i) == "*")
		node += "*"
		i++

	else if(token(i)[1] == "/")
		i = object_type(i, node)

	else
		i = parse_error("Expected '*' or type path for select item")

	return i

// Standardized method for handling the IN/FROM and WHERE options.
/datum/sdql_parser/proc/selectors(i, list/node)
	while (token(i))
		var/tok = tokenl(i)
		if (tok in list("from", "in"))
			var/list/from = list()
			i = from_item(i + 1, from)

			node["from"] = from
			continue

		if (tok == "where")
			var/list/where = list()
			i = bool_expression(i + 1, where)

			node["where"] = where
			continue

		parse_error("Expected either FROM, IN or WHERE token, found [token(i)] instead.")
		return i + 1

	if (!node.Find("from"))
		node["from"] = list("world")

	return i

//from_item: 'world' | expression
/datum/sdql_parser/proc/from_item(i, list/node)
	if(token(i) == "world")
		node += "world"
		i++

	else
		i = expression(i, node)

	return i

//bool_expression: expression [bool_operator bool_expression]
/datum/sdql_parser/proc/bool_expression(i, list/node)

	var/list/bool = list()
	i = expression(i, bool)

	node[++node.len] = bool

	if(tokenl(i) in boolean_operators)
		i = bool_operator(i, node)
		i = bool_expression(i, node)

	return i

//assignment: <variable name> '=' expression
/datum/sdql_parser/proc/assignment(i, list/node, list/assignment_list = list())
	assignment_list += token(i)

	if(token(i + 1) == ".")
		i = assignment(i + 2, node, assignment_list)

	else if(token(i + 1) == "=")
		var/exp_list = list()
		node[assignment_list] = exp_list

		i = expression(i + 2, exp_list)

	else
		parse_error("Assignment expected, but no = found")

	return i

//variable: <variable name> | variable '.' variable | variable '[' <list index> ']' | '{' <ref as hex number> '}' | '(' expression ')' | call_function
/datum/sdql_parser/proc/variable(i, list/node)
	var/list/L = list(token(i))
	node[++node.len] = L

	if(token(i) == "{")
		L += token(i + 1)
		i += 2

		if(token(i) != "}")
			parse_error("Missing } at end of pointer.")

	else if(token(i) == "(") // not a proc but an expression
		var/list/sub_expression = list()

		i = expression(i + 1, sub_expression)

		if(token(i) != ")")
			parse_error("Missing ) at end of expression.")

		L[++L.len] = sub_expression

	if(token(i + 1) == ".")
		L += "."
		i = variable(i + 2, L)

	else if (token(i + 1) == "(") // OH BOY PROC
		var/list/arguments = list()
		i = call_function(i, null, arguments)
		L += ":"
		L[++L.len] = arguments

	else if (token(i + 1) == "\[")
		var/list/expression = list()
		i = expression(i + 2, expression)
		if (token(i) != "]")
			parse_error("Missing ] at the end of list access.")

		L += "\["
		L[++L.len] = expression
		i++

	else
		i++

	return i

//object_type: <type path>
/datum/sdql_parser/proc/object_type(i, list/node)

	if(token(i)[1] != "/")
		return parse_error("Expected type, but it didn't begin with /")

	var/path = text2path(token(i))
	if (path == null)
		return parse_error("Nonexistent type path: [token(i)]")

	node += path

	return i + 1

//comparator: '=' | '==' | '!=' | '<>' | '<' | '<=' | '>' | '>='
/datum/sdql_parser/proc/comparator(i, list/node)

	if(token(i) in list("=", "==", "!=", "<>", "<", "<=", ">", ">="))
		node += token(i)

	else
		parse_error("Unknown comparator [token(i)]")

	return i + 1

//bool_operator: 'AND' | '&&' | 'OR' | '||'
/datum/sdql_parser/proc/bool_operator(i, list/node)

	if(tokenl(i) in list("and", "or", "&&", "||"))
		node += token(i)

	else
		parse_error("Unknown comparator [token(i)]")

	return i + 1

//string: ''' <some text> ''' | '"' <some text > '"'
/datum/sdql_parser/proc/string(i, list/node)

	if(token(i)[1] in list("'", "\""))
		node += token(i)

	else
		parse_error("Expected string but found '[token(i)]'")

	return i + 1

//array: '[' expression_list ']'
/datum/sdql_parser/proc/array(i, list/node)
	// Arrays get turned into this: list("[", list(exp_1a = exp_1b, ...), ...), "[" is to mark the next node as an array.
	if(token(i)[1] != "\[")
		parse_error("Expected an array but found '[token(i)]'")
		return i + 1

	node += token(i) // Add the "["

	var/list/expression_list = list()

	i++
	if(token(i) != "]")
		var/list/temp_expression_list = list()
		var/tok
		do
			tok = token(i)
			if (tok == "," || tok == ":")
				if (temp_expression_list == null)
					parse_error("Found ',' or ':' without expression in an array.")
					return i + 1

				expression_list[++expression_list.len] = temp_expression_list
				temp_expression_list = null
				if (tok == ":")
					temp_expression_list = list()
					i = expression(i + 1, temp_expression_list)
					expression_list[expression_list[expression_list.len]] = temp_expression_list
					temp_expression_list = null
					tok = token(i)
					if (tok != ",")
						if (tok == "]")
							break

						parse_error("Expected ',' or ']' after array assoc value, but found '[token(i)]'")
						return i

				i++
				continue

			temp_expression_list = list()
			i = expression(i, temp_expression_list)

		while(token(i) && token(i) != "]")

		if (temp_expression_list)
			expression_list[++expression_list.len] = temp_expression_list

	node[++node.len] = expression_list

	return i + 1

//selectors_array: '@[' object_selectors ']'
/datum/sdql_parser/proc/selectors_array(i, list/node)
	if(token(i) == "@\[")
		node += token(i++)
		if(token(i) != "]")
			var/list/select = list()
			i = object_selectors(i, select)
			node[++node.len] = select
			if(token(i) != "]")
				parse_error("Expected ']' to close selector array, but found '[token(i)]'")
		else
			parse_error("Selector array expected a selector, but found nothing")
	else
		parse_error("Expected '@\[' but found '[token(i)]'")

	return i + 1

//call_function: <function name> ['(' [arguments] ')']
/datum/sdql_parser/proc/call_function(i, list/node, list/arguments)
	if(length(tokenl(i)))
		var/procname = ""
		if(tokenl(i) == "global" && token(i + 1) == ".") // Global proc.
			i += 2
			procname = "global."
		node += procname + token(i++)
		if(token(i) != "(")
			parse_error("Expected ( but found '[token(i)]'")

		else if(token(i + 1) != ")")
			var/list/temp_expression_list = list()
			do
				i = expression(i + 1, temp_expression_list)
				if(token(i) == ",")
					arguments[++arguments.len] = temp_expression_list
					temp_expression_list = list()
					continue

			while(token(i) && token(i) != ")")

			arguments[++arguments.len] = temp_expression_list // The code this is copy pasted from won't be executed when it's the last param, this fixes that.
		else
			i++
	else
		parse_error("Expected a function but found nothing")
	return i + 1

//expression: ( unary_expression | value ) [binary_operator expression]
/datum/sdql_parser/proc/expression(i, list/node)

	if(token(i) in unary_operators)
		i = unary_expression(i, node)

	else
		i = value(i, node)

	if(token(i) in binary_operators)
		i = binary_operator(i, node)
		i = expression(i, node)

	else if(token(i) in comparators)
		i = binary_operator(i, node)

		var/list/rhs = list()
		i = expression(i, rhs)

		node[++node.len] = rhs

	return i

//unary_expression: unary_operator ( unary_expression | value )
/datum/sdql_parser/proc/unary_expression(i, list/node)

	if(token(i) in unary_operators)
		var/list/unary_exp = list()

		unary_exp += token(i)
		i++

		if(token(i) in unary_operators)
			i = unary_expression(i, unary_exp)

		else
			i = value(i, unary_exp)

		node[++node.len] = unary_exp

	else
		parse_error("Expected unary operator but found '[token(i)]'")

	return i

//binary_operator: comparator | '+' | '-' | '/' | '*' | '&' | '|' | '^' | '%'
/datum/sdql_parser/proc/binary_operator(i, list/node)

	if(token(i) in (binary_operators + comparators))
		node += token(i)

	else
		parse_error("Unknown binary operator [token(i)]")

	return i + 1

//value: variable | string | number | 'null' | object_type | array | selectors_array
/datum/sdql_parser/proc/value(i, list/node)
	if(token(i) == "null")
		node += "null"
		i++

	else if(lowertext(copytext(token(i), 1, 3)) == "0x" && isnum(hex2num(copytext(token(i), 3))))//3 == length("0x") + 1
		node += hex2num(copytext(token(i), 3))
		i++

	else if(isnum(text2num(token(i))))
		node += text2num(token(i))
		i++

	else if(token(i)[1] in list("'", "\""))
		i = string(i, node)

	else if(token(i)[1] == "\[") // Start a list.
		i = array(i, node)

	else if(copytext(token(i), 1, 3) == "@\[")//3 == length("@\[") + 1
		i = selectors_array(i, node)

	else if(token(i)[1] == "/")
		i = object_type(i, node)

	else
		i = variable(i, node)

	return i

repo_name: AnalWerewolf/sundown

license: mit


code:
// Wrappers for BYOND default procs which can't directly be called by call().

/proc/_abs(A)
	return abs(A)

/proc/_animate(atom/A, set_vars, time = 10, loop = 1, easing = LINEAR_EASING, flags = null)
	var/mutable_appearance/MA = new()
	for(var/v in set_vars)
		MA.vars[v] = set_vars[v]
	animate(A, appearance = MA, time, loop, easing, flags)

/proc/_arccos(A)
	return arccos(A)

/proc/_arcsin(A)
	return arcsin(A)

/proc/_ascii2text(A)
	return ascii2text(A)

/proc/_block(Start, End)
	return block(Start, End)

/proc/_ckey(Key)
	return ckey(Key)

/proc/_ckeyEx(Key)
	return ckeyEx(Key)

/proc/_copytext(T, Start = 1, End = 0)
	return copytext(T, Start, End)

/proc/_cos(X)
	return cos(X)

/proc/_get_dir(Loc1, Loc2)
	return get_dir(Loc1, Loc2)

/proc/_get_dist(Loc1, Loc2)
	return get_dist(Loc1, Loc2)

/proc/_get_step(Ref, Dir)
	return get_step(Ref, Dir)

/proc/_hearers(Depth = world.view, Center = usr)
	return hearers(Depth, Center)

/proc/_image(icon, loc, icon_state, layer, dir)
	return image(icon, loc, icon_state, layer, dir)

/proc/_istype(object, type)
	return istype(object, type)

/proc/_ispath(path, type)
	return ispath(path, type)

/proc/_length(E)
	return length(E)

/proc/_link(thing, url)
	thing << link(url)

/proc/_locate(X, Y, Z)
	if (isnull(Y)) // Assuming that it's only a single-argument call.
		return locate(X)

	return locate(X, Y, Z)

/proc/_log(X, Y)
	return log(X, Y)

/proc/_lowertext(T)
	return lowertext(T)

/proc/_matrix(a, b, c, d, e, f)
	return matrix(a, b, c, d, e, f)

/proc/_max(...)
	return max(arglist(args))

/proc/_md5(T)
	return md5(T)

/proc/_min(...)
	return min(arglist(args))

/proc/_new(type, arguments)
	var/datum/result = new type(arglist(arguments))
	if(istype(result))
		result.datum_flags |= DF_VAR_EDITED
	return result

/proc/_num2text(N, SigFig = 6)
	return num2text(N, SigFig)

/proc/_text2num(T)
	return text2num(T)

/proc/_ohearers(Dist, Center = usr)
	return ohearers(Dist, Center)

/proc/_orange(Dist, Center = usr)
	return orange(Dist, Center)

/proc/_output(thing, msg, control)
	thing << output(msg, control)

/proc/_oview(Dist, Center = usr)
	return oview(Dist, Center)

/proc/_oviewers(Dist, Center = usr)
	return oviewers(Dist, Center)

/proc/_params2list(Params)
	return params2list(Params)

/proc/_pick(...)
	return pick(arglist(args))

/proc/_prob(P)
	return prob(P)

/proc/_rand(L = 0, H = 1)
	return rand(L, H)

/proc/_range(Dist, Center = usr)
	return range(Dist, Center)

/proc/_regex(pattern, flags)
	return regex(pattern, flags)

/proc/_REGEX_QUOTE(text)
	return REGEX_QUOTE(text)

/proc/_REGEX_QUOTE_REPLACEMENT(text)
	return REGEX_QUOTE_REPLACEMENT(text)

/proc/_replacetext(Haystack, Needle, Replacement, Start = 1,End = 0)
	return replacetext(Haystack, Needle, Replacement, Start, End)

/proc/_replacetextEx(Haystack, Needle, Replacement, Start = 1,End = 0)
	return replacetextEx(Haystack, Needle, Replacement, Start, End)

/proc/_rgb(R, G, B)
	return rgb(R, G, B)

/proc/_rgba(R, G, B, A)
	return rgb(R, G, B, A)

/proc/_roll(dice)
	return roll(dice)

/proc/_round(A, B = 1)
	return round(A, B)

/proc/_sin(X)
	return sin(X)

/proc/_list_add(list/L, ...)
	if (args.len < 2)
		return
	L += args.Copy(2)

/proc/_list_copy(list/L, Start = 1, End = 0)
	return L.Copy(Start, End)

/proc/_list_cut(list/L, Start = 1, End = 0)
	L.Cut(Start, End)

/proc/_list_find(list/L, Elem, Start = 1, End = 0)
	return L.Find(Elem, Start, End)

/proc/_list_insert(list/L, Index, Item)
	return L.Insert(Index, Item)

/proc/_list_join(list/L, Glue, Start = 0, End = 1)
	return L.Join(Glue, Start, End)

/proc/_list_remove(list/L, ...)
	if (args.len < 2)
		return
	L -= args.Copy(2)

/proc/_list_set(list/L, key, value)
	L[key] = value

/proc/_list_get(list/L, key)
	return L[key]

/proc/_list_numerical_add(L, key, num)
	L[key] += num

/proc/_list_swap(list/L, Index1, Index2)
	L.Swap(Index1, Index2)

/proc/_walk(ref, dir, lag)
	walk(ref, dir, lag)

/proc/_walk_towards(ref, trg, lag)
	walk_towards(ref, trg, lag)

/proc/_walk_to(ref, trg, min, lag)
	walk_to(ref, trg, min, lag)

/proc/_walk_away(ref, trg, max, lag)
	walk_away(ref, trg, max, lag)

/proc/_walk_rand(ref, lag)
	walk_rand(ref, lag)

/proc/_step(ref, dir)
	step(ref, dir)

/proc/_step_rand(ref)
	step_rand(ref)

/proc/_step_to(ref, trg, min)
	step_to(ref, trg, min)

/proc/_step_towards(ref, trg)
	step_towards(ref, trg)

/proc/_step_away(ref, trg, max)
	step_away(ref, trg, max)

/proc/_has_trait(datum/thing,trait)
	return HAS_TRAIT(thing,trait)

/proc/_add_trait(datum/thing,trait,source)
	ADD_TRAIT(thing,trait,source)

/proc/_remove_trait(datum/thing,trait,source)
	REMOVE_TRAIT(thing,trait,source)


code:
/datum/component/sdql_executor
	var/query = "CALL visible_message(\"The spell fizzles!\") ON * IN TARGETS"
	var/giver //The ckey of the user that gave this spell
	var/suppress_message_admins
	var/list/scratchpad = list() //Use this to store vars in between queries and casts.
	var/list/saved_overrides = list()

/datum/component/sdql_executor/Initialize(giver)
	src.giver = giver

//Returns the address of x without the square brackets around it.
#define RAW_ADDRESS(x) copytext("\ref[x]",2,-1)

/datum/component/sdql_executor/proc/execute(list/targets, mob/user)
	if(!CONFIG_GET(flag/sdql_spells))
		return
	if(!length(query))
		return
	var/query_text = query
	var/message_query = query
	var/list/targets_and_user_list = list(user)
	if(targets)
		targets_and_user_list += targets
	var/targets_and_user_string = ref_list(targets_and_user_list)
	var/targets_string = ref_list(targets)
	query_text = replacetextEx_char(query_text, "TARGETS_AND_USER", "[targets_and_user_string]")
	message_query = replacetext_char(message_query, "TARGETS_AND_USER", (targets_and_user_list.len > 3) ? "\[[targets_and_user_list.len] items]" : targets_and_user_string)
	query_text = replacetextEx_char(query_text, "USER", "{[RAW_ADDRESS(user)]}")
	message_query = replacetextEx_char(message_query, "USER", "{[RAW_ADDRESS(user)]}")
	query_text = replacetextEx_char(query_text, "TARGETS", "[targets_string]")
	message_query = replacetextEx_char(message_query, "TARGETS", (targets?.len > 3) ? "\[[targets.len] items]" : targets_string)
	query_text = replacetextEx_char(query_text, "SOURCE", "{[RAW_ADDRESS(parent)]}")
	message_query = replacetextEx_char(message_query, "SOURCE", "{[RAW_ADDRESS(parent)]}")
	query_text = replacetextEx_char(query_text, "SCRATCHPAD", "({[RAW_ADDRESS(src)]}.scratchpad)")
	message_query = replacetextEx_char(message_query, "SCRATCHPAD", "({[RAW_ADDRESS(src)]}.scratchpad)")
	if(!usr) //We need to set AdminProcCaller manually because it won't be set automatically by WrapAdminProcCall if usr is null
		GLOB.AdminProcCaller = "SDQL_SPELL_OF_[user.ckey]"
	GLOB.AdminProcCallCount++
	world.SDQL2_query(query_text, "[key_name(user, TRUE)] (via an SDQL spell given by [giver])", "[key_name(user)] (via an SDQL spell given by [giver])", silent = suppress_message_admins)
	GLOB.AdminProcCallCount--
	GLOB.AdminProcCaller = null

/datum/component/sdql_executor/proc/ref_list(list/L)
	if(isnull(L) || !L.len)
		return "\[]"
	var/ret = "\["
	for(var/i in 1 to L.len-1)
		ret += "{[RAW_ADDRESS(L[i])]},"
	ret += "{[RAW_ADDRESS(L[L.len])]}]"
	return ret

#undef RAW_ADDRESS


code:
/// An admin verb to view all sdql spells, plus useful information
/datum/admins/proc/view_all_sdql_spells()
	set category = "Admin.Game"
	set name = "View All SDQL Spells"

	if(CONFIG_GET(flag/sdql_spells) || tgui_alert(usr, "SDQL spells are disabled. Open the admin panel anyways?", "SDQL Admin Panel", list("Yes", "No")) == "Yes")
		var/static/datum/SDQL_spell_panel/SDQL_spell_panel = new
		SDQL_spell_panel.ui_interact(usr)

/datum/SDQL_spell_panel

/datum/SDQL_spell_panel/ui_static_data(mob/user)
	var/list/data = list()
	data["spells"] = list()

	for (var/obj/effect/proc_holder/spell/spell as anything in GLOB.sdql_spells)
		var/mob/living/owner = spell.owner.resolve()
		var/datum/component/sdql_executor/executor = spell.GetComponent(/datum/component/sdql_executor)
		if(!executor)
			continue

		data["spells"] += list(list(
			"ref" = REF(spell),
			"name" = "[spell]",
			"owner" = owner,
			"ownerRef" = REF(owner),
			"creator" = executor.giver
		))

	return data

/datum/SDQL_spell_panel/ui_act(action, list/params)
	. = ..()
	if (.)
		return .

	switch(action)
		if("edit_spell")
			var/obj/effect/proc_holder/spell/spell = locate(params["spell"])
			if(!spell)
				to_chat(usr, span_warning("That spell no longer exists!"))
				return
			var/datum/component/sdql_executor/executor = spell.GetComponent(/datum/component/sdql_executor)
			if(!executor)
				to_chat(usr, span_warning("[spell][spell.p_s()] SDQL executor component is gone!"))
				return
			if(usr.ckey == executor.giver || tgui_alert(usr, "You didn't create this SDQL spell. Edit it anyways?", "SDQL Admin Panel", list("Yes", "No")) == "Yes")
				usr.client?.cmd_sdql_spell_menu(spell)
		if("follow_owner")
			var/mob/living/owner = locate(params["owner"])
			if(!owner)
				to_chat(usr, span_warning("That mob no longer exists!"))
				return
			usr.client?.admin_follow(owner)
		if("vv_spell")
			var/obj/effect/proc_holder/spell/spell = locate(params["spell"])
			if(!spell)
				to_chat(usr, span_warning("That spell no longer exists!"))
				return
			usr.client?.debug_variables(spell)
		if("open_player_panel")
			var/mob/living/owner = locate(params["owner"])
			if(!owner)
				to_chat(usr, span_warning("That mob no longer exists!"))
				return
			usr.client?.holder?.show_player_panel(owner)

	return TRUE

/datum/SDQL_spell_panel/ui_state(mob/user)
	return GLOB.admin_state

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