mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-01-01 20:42:08 +00:00
Implements https://hackmd.io/@tgstation/SkeUS7lSp , rewriting Dynamic
from the ground-up
- Dynamic configuration is now vastly streamlined, making it far far far
easier to understand and edit
- Threat is gone entirely; round chaos is now determined by dynamic
tiers
- There's 5 dynamic tiers, 0 to 4.
- 0 is a pure greenshift.
- Tiers are just picked via weight - "16% chance of getting a high chaos
round".
- Tiers have min pop ranges. "Tier 4 (high chaos) requires 25 pop to be
selected".
- Tier determines how much of every ruleset is picked. "Tier 4 (High
Chaos) will pick 3-4 roundstart[1], 1-2 light, 1-2 heavy, and 2-3
latejoins".
- The number of rulesets picked depends on how many people are in the
server - this is also configurable[2]. As an example, a tier that
demands "1-3" rulesets will not spawn 3 rulesets if population <= 40 and
will not spawn 2 rulesets if population <= 25.
- Tiers also determine time before light, heavy, and latejoin rulesets
are picked, as well as the cooldown range between spawns. More chaotic
tiers may send midrounds sooner or wait less time between sending them.
- On the ruleset side of things, "requirements", "scaling", and
"enemies" is gone.
- You can configure a ruleset's min pop and weight flat, or per tier.
- For example a ruleset like Obsession is weighted higher for tiers 1-2
and lower for tiers 3-4.
- Rather than scaling up, roundstart rulesets can just be selected
multiple times.
- Rulesets also have `min_antag_cap` and `max_antag_cap`.
`min_antag_cap` determines how many candidates are needed for it to run,
and `max_antag_cap` determines how many candidates are selected.
- Rulesets attempt to run every 2.5 minutes. [3]
- Light rulesets will ALWAYS be picked before heavy rulesets. [4]
- Light injection chance is no longer 100%, heavy injection chance
formula has been simplified.
- Chance simply scales based on number of dead players / total number
off players, with a flag 50% chance if no antags exist. [5]
[1] This does not guarantee you will actually GET 3-4 roundstart
rulesets. If a roundstart ruleset is picked, and it ends up being unable
to execute (such as "not enough candidates", that slot is effectively a
wash.) This might be revisited.
[2] Currently, this is a hard limit - below X pop, you WILL get a
quarter or a half of the rulesets. This might be revisited to just be
weighted - you are just MORE LIKELY to get a quarter or a half.
[3] Little worried about accidentally frontloading everything so we'll
see about this
[4] This may be revisited but in most contexts it seems sensible.
[5] This may also be revisited, I'm not 100% sure what the best / most
simple way to tackle midround chances is.
Other implementation details
- The process of making rulesets has been streamlined as well. Many
rulesets only amount to a definition and `assign_role`.
- Dynamic.json -> Dynamic.toml
- Dynamic event hijacked was ripped out entirely.
- Most midround antag random events are now dynamic rulesets. Fugitives,
Morphs, Slaughter Demons, etc.
- The 1 weight slaughter demon event is gone. RIP in peace.
- There is now a hidden midround event that simply adds +1 latejoin, +1
light, or +1 heavy ruleset.
- `mind.special_role` is dead. Minds have a lazylist of special roles
now but it's essentially only used for traitor panel.
- Revs refactored almost entirely. Revs can now exist without a dynamic
ruleset.
- Cult refactored a tiny bit.
- Antag datums cleaned up.
- Pre round setup is less centralized on Dynamic.
- Admins have a whole panel for interfacing with dynamic. It's pretty
slapdash I'm sure someone could make a nicer looking one.


- Maybe some other things.
See readme for more info.
Will you see a massive change in how rounds play out? My hunch says
rounds will spawn less rulesets on average, but it's ultimately to how
it's configured
🆑 Melbert
refactor: Dynamic rewritten entirely, report any strange rounds
config: Dynamic config reworked, it's now a TOML file
refactor: Refactored antag roles somewhat, report any oddities
refactor: Refactored Revolution entirely, report any oddities
del: Deleted most midround events that spawn antags - they use dynamic
rulesets now
add: Dynamic rulesets can now be false alarms
add: Adds a random event that gives dynamic the ability to run another
ruleset later
admin: Adds a panel for messing around with dynamic
admin: Adds a panel for chance for every dynamic ruleset to be selected
admin: You can spawn revs without using dynamic now
fix: Nuke team leaders get their fun title back
/🆑
(cherry picked from commit 4c277dc572)
1204 lines
45 KiB
Plaintext
1204 lines
45 KiB
Plaintext
/// Client var used for returning the ahelp verb
|
|
/client/var/adminhelptimerid = 0
|
|
/// Client var used for tracking the ticket the (usually) not-admin client is dealing with
|
|
/client/var/datum/admin_help/current_ticket
|
|
|
|
GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
|
|
|
|
/**
|
|
* # Adminhelp Ticket Manager
|
|
*/
|
|
/datum/admin_help_tickets
|
|
/// The set of all active tickets
|
|
var/list/active_tickets = list()
|
|
/// The set of all closed tickets
|
|
var/list/closed_tickets = list()
|
|
/// The set of all resolved tickets
|
|
var/list/resolved_tickets = list()
|
|
|
|
var/obj/effect/statclick/ticket_list/astatclick = new(null, null, AHELP_ACTIVE)
|
|
var/obj/effect/statclick/ticket_list/cstatclick = new(null, null, AHELP_CLOSED)
|
|
var/obj/effect/statclick/ticket_list/rstatclick = new(null, null, AHELP_RESOLVED)
|
|
|
|
/datum/admin_help_tickets/Destroy()
|
|
QDEL_LIST(active_tickets)
|
|
QDEL_LIST(closed_tickets)
|
|
QDEL_LIST(resolved_tickets)
|
|
QDEL_NULL(astatclick)
|
|
QDEL_NULL(cstatclick)
|
|
QDEL_NULL(rstatclick)
|
|
return ..()
|
|
|
|
/datum/admin_help_tickets/proc/TicketByID(id)
|
|
var/list/lists = list(active_tickets, closed_tickets, resolved_tickets)
|
|
for(var/I in lists)
|
|
for(var/J in I)
|
|
var/datum/admin_help/AH = J
|
|
if(AH.id == id)
|
|
return J
|
|
|
|
/datum/admin_help_tickets/proc/TicketsByCKey(ckey)
|
|
. = list()
|
|
var/list/lists = list(active_tickets, closed_tickets, resolved_tickets)
|
|
for(var/I in lists)
|
|
for(var/J in I)
|
|
var/datum/admin_help/AH = J
|
|
if(AH.initiator_ckey == ckey)
|
|
. += AH
|
|
|
|
//private
|
|
/datum/admin_help_tickets/proc/ListInsert(datum/admin_help/new_ticket)
|
|
var/list/ticket_list
|
|
switch(new_ticket.state)
|
|
if(AHELP_ACTIVE)
|
|
ticket_list = active_tickets
|
|
if(AHELP_CLOSED)
|
|
ticket_list = closed_tickets
|
|
if(AHELP_RESOLVED)
|
|
ticket_list = resolved_tickets
|
|
else
|
|
CRASH("Invalid ticket state: [new_ticket.state]")
|
|
var/num_closed = ticket_list.len
|
|
if(num_closed)
|
|
for(var/I in 1 to num_closed)
|
|
var/datum/admin_help/AH = ticket_list[I]
|
|
if(AH.id > new_ticket.id)
|
|
ticket_list.Insert(I, new_ticket)
|
|
return
|
|
ticket_list += new_ticket
|
|
|
|
//opens the ticket listings for one of the 3 states
|
|
/datum/admin_help_tickets/proc/BrowseTickets(state)
|
|
var/list/l2b
|
|
var/title
|
|
switch(state)
|
|
if(AHELP_ACTIVE)
|
|
l2b = active_tickets
|
|
title = "Active Tickets"
|
|
if(AHELP_CLOSED)
|
|
l2b = closed_tickets
|
|
title = "Closed Tickets"
|
|
if(AHELP_RESOLVED)
|
|
l2b = resolved_tickets
|
|
title = "Resolved Tickets"
|
|
if(!l2b)
|
|
return
|
|
var/list/dat = list("<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>[title]</title></head>")
|
|
dat += "<A href='byond://?_src_=holder;[HrefToken()];ahelp_tickets=[state]'>Refresh</A><br><br>"
|
|
for(var/I in l2b)
|
|
var/datum/admin_help/AH = I
|
|
dat += "[span_adminnotice("[span_adminhelp("Ticket #[AH.id]")]: <A href='byond://?_src_=holder;[HrefToken()];ahelp=[REF(AH)];ahelp_action=ticket'>[AH.initiator_key_name]: [AH.name]</A>")]<br>"
|
|
|
|
usr << browse(dat.Join(), "window=ahelp_list[state];size=600x480")
|
|
|
|
//Tickets statpanel
|
|
/datum/admin_help_tickets/proc/stat_entry()
|
|
SHOULD_CALL_PARENT(TRUE)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
var/list/L = list()
|
|
var/num_disconnected = 0
|
|
L[++L.len] = list("Active Tickets:", "[astatclick.update("[active_tickets.len]")]", null, REF(astatclick))
|
|
astatclick.update("[active_tickets.len]")
|
|
for(var/I in active_tickets)
|
|
var/datum/admin_help/AH = I
|
|
if(AH.initiator)
|
|
var/obj/effect/statclick/updated = AH.statclick.update()
|
|
L[++L.len] = list("[AH.handler ? "H-[AH.handler]" : ""]#[AH.id]. [AH.initiator_key_name]:", "[updated.name]", REF(AH)) //SKYRAT EDIT CHANGE - ADMIN
|
|
else
|
|
++num_disconnected
|
|
if(num_disconnected)
|
|
L[++L.len] = list("Disconnected:", "[astatclick.update("[num_disconnected]")]", null, REF(astatclick))
|
|
L[++L.len] = list("Closed Tickets:", "[cstatclick.update("[closed_tickets.len]")]", null, REF(cstatclick))
|
|
L[++L.len] = list("Resolved Tickets:", "[rstatclick.update("[resolved_tickets.len]")]", null, REF(rstatclick))
|
|
//SKYRAT EDIT ADDITION
|
|
if(LAZYLEN(SSopposing_force.submitted_applications))
|
|
for(var/datum/opposing_force/opposing_force as anything in SSopposing_force.submitted_applications)
|
|
L[++L.len] = list("[opposing_force.handling_admin ? "H-[opposing_force.handling_admin]. " : ""]OPFOR:", "[opposing_force.stat_button.update(" [opposing_force.ckey] ")]", null, REF(opposing_force.stat_button))
|
|
//SKYRAT EDIT END
|
|
return L
|
|
|
|
//Reassociate still open ticket if one exists
|
|
/datum/admin_help_tickets/proc/ClientLogin(client/C)
|
|
C.current_ticket = CKey2ActiveTicket(C.ckey)
|
|
if(C.current_ticket)
|
|
C.current_ticket.initiator = C
|
|
C.current_ticket.AddInteraction("Client reconnected.")
|
|
SSblackbox.LogAhelp(C.current_ticket.id, "Reconnected", "Client reconnected", C.ckey)
|
|
|
|
//Dissasociate ticket
|
|
/datum/admin_help_tickets/proc/ClientLogout(client/C)
|
|
if(C.current_ticket)
|
|
var/datum/admin_help/T = C.current_ticket
|
|
T.AddInteraction("Client disconnected.")
|
|
//Gotta async this cause clients only logout on destroy, and sleeping in destroy is disgusting
|
|
INVOKE_ASYNC(SSblackbox, TYPE_PROC_REF(/datum/controller/subsystem/blackbox, LogAhelp), T.id, "Disconnected", "Client disconnected", C.ckey)
|
|
T.initiator = null
|
|
|
|
//Get a ticket given a ckey
|
|
/datum/admin_help_tickets/proc/CKey2ActiveTicket(ckey)
|
|
for(var/I in active_tickets)
|
|
var/datum/admin_help/AH = I
|
|
if(AH.initiator_ckey == ckey)
|
|
return AH
|
|
|
|
//
|
|
//TICKET LIST STATCLICK
|
|
//
|
|
|
|
/obj/effect/statclick/ticket_list
|
|
var/current_state
|
|
|
|
/obj/effect/statclick/ticket_list/Initialize(mapload, name, state)
|
|
. = ..()
|
|
current_state = state
|
|
|
|
/obj/effect/statclick/ticket_list/Click()
|
|
if (!usr.client?.holder)
|
|
message_admins("[key_name_admin(usr)] non-holder clicked on a ticket list statclick! ([src])")
|
|
usr.log_message("non-holder clicked on a ticket list statclick! ([src])", LOG_ADMIN)
|
|
return
|
|
|
|
GLOB.ahelp_tickets.BrowseTickets(current_state)
|
|
|
|
//called by admin topic
|
|
/obj/effect/statclick/ticket_list/proc/Action()
|
|
Click()
|
|
|
|
#define WEBHOOK_NONE 0
|
|
#define WEBHOOK_URGENT 1
|
|
#define WEBHOOK_NON_URGENT 2
|
|
|
|
/**
|
|
* # Adminhelp Ticket
|
|
*/
|
|
/datum/admin_help
|
|
/// Unique ID of the ticket
|
|
var/id
|
|
/// The current name of the ticket
|
|
var/name
|
|
/// The current state of the ticket
|
|
var/state = AHELP_ACTIVE
|
|
/// The time at which the ticket was opened
|
|
var/opened_at
|
|
/// The time at which the ticket was closed
|
|
var/closed_at
|
|
/// Semi-misnomer, it's the person who ahelped/was bwoinked
|
|
var/client/initiator
|
|
/// The ckey of the initiator
|
|
var/initiator_ckey
|
|
/// The key name of the initiator
|
|
var/initiator_key_name
|
|
/// If any admins were online when the ticket was initialized
|
|
var/heard_by_no_admins = FALSE
|
|
/// The collection of interactions with this ticket. Use AddInteraction() or, preferably, admin_ticket_log()
|
|
var/list/ticket_interactions
|
|
/// Statclick holder for the ticket
|
|
var/obj/effect/statclick/ahelp/statclick
|
|
/// Static counter used for generating each ticket ID
|
|
var/static/ticket_counter = 0
|
|
/// The list of clients currently responding to the opening ticket before it gets a response
|
|
var/list/opening_responders
|
|
/// Whether this ahelp has sent a webhook or not, and what type
|
|
var/webhook_sent = WEBHOOK_NONE
|
|
/// List of player interactions
|
|
var/list/player_interactions
|
|
/// List of admin ckeys that are involved, like through responding
|
|
var/list/admins_involved = list()
|
|
/// Has the player replied to this ticket yet?
|
|
var/player_replied = FALSE
|
|
|
|
/**
|
|
* Call this on its own to create a ticket, don't manually assign current_ticket
|
|
*
|
|
* Arguments:
|
|
* * msg_raw - The first message of this admin_help: used for the initial title of the ticket
|
|
* * is_bwoink - Boolean operator, TRUE if this ticket was started by an admin PM
|
|
*/
|
|
/datum/admin_help/New(msg_raw, client/C, is_bwoink, client/admin_C, urgent = FALSE) //SKYRAT EDIT CHANGE
|
|
//clean the input msg
|
|
var/msg = sanitize(copytext_char(msg_raw, 1, MAX_MESSAGE_LEN))
|
|
if(!msg || !C || !C.mob)
|
|
qdel(src)
|
|
return
|
|
|
|
//SKYRAT EDIT ADDITION BEGIN
|
|
if(admin_C && is_bwoink)
|
|
handler = "[admin_C.ckey]"
|
|
//SKYRAT EDIT END
|
|
|
|
id = ++ticket_counter
|
|
opened_at = world.time
|
|
|
|
name = copytext_char(msg, 1, 100)
|
|
|
|
full_text = msg //SKYRAT EDIT ADDITION - Adminhelps into mentorhelps converting.
|
|
|
|
initiator = C
|
|
initiator_ckey = initiator.ckey
|
|
initiator_key_name = key_name(initiator, FALSE, TRUE)
|
|
if(initiator.current_ticket) //This is a bug
|
|
stack_trace("Multiple ahelp current_tickets")
|
|
initiator.current_ticket.AddInteraction("Ticket erroneously left open by code")
|
|
initiator.current_ticket.Close()
|
|
initiator.current_ticket = src
|
|
|
|
TimeoutVerb()
|
|
|
|
statclick = new(null, src)
|
|
ticket_interactions = list()
|
|
player_interactions = list()
|
|
|
|
addtimer(CALLBACK(src, PROC_REF(add_to_ping_ss), 2 MINUTES)) //SKYRAT EDIT ADDITION - Ticket Ping | this is not responsible for the notification itself, but only for adding the ticket to the list of those to notify.
|
|
if(is_bwoink)
|
|
AddInteraction("<font color='blue'>[key_name_admin(usr)] PM'd [LinkedReplyName()]</font>", player_message = "<font color='blue'>[key_name_admin(usr, include_name = FALSE)] PM'd [LinkedReplyName()]</font>")
|
|
message_admins("<font color='blue'>Ticket [TicketHref("#[id]")] created</font>")
|
|
else
|
|
MessageNoRecipient(msg_raw, urgent)
|
|
send_message_to_tgs(msg, urgent)
|
|
GLOB.ahelp_tickets.active_tickets += src
|
|
|
|
/datum/admin_help/proc/format_embed_discord(message)
|
|
var/datum/discord_embed/embed = new()
|
|
embed.title = "Ticket #[id]"
|
|
embed.description = "<byond://[world.internet_address]:[world.port]>"
|
|
embed.author = key_name(initiator_ckey)
|
|
var/round_state
|
|
switch(SSticker.current_state)
|
|
if(GAME_STATE_STARTUP, GAME_STATE_PREGAME, GAME_STATE_SETTING_UP)
|
|
round_state = "Round has not started"
|
|
if(GAME_STATE_PLAYING)
|
|
round_state = "Round is ongoing."
|
|
if(SSshuttle.emergency.getModeStr())
|
|
round_state += "\n[SSshuttle.emergency.getModeStr()]: [SSshuttle.emergency.getTimerStr()]"
|
|
if(SSticker.emergency_reason)
|
|
round_state += ", Shuttle call reason: [SSticker.emergency_reason]"
|
|
if(GAME_STATE_FINISHED)
|
|
round_state = "Round has ended"
|
|
var/list/admin_counts = get_admin_counts(R_BAN)
|
|
var/stealth_admins = jointext(admin_counts["stealth"], ", ")
|
|
var/afk_admins = jointext(admin_counts["afk"], ", ")
|
|
var/other_admins = jointext(admin_counts["noflags"], ", ")
|
|
var/admin_text = ""
|
|
var/player_count = "**Total**: [length(GLOB.clients)], **Living**: [length(GLOB.alive_player_list)], **Dead**: [length(GLOB.dead_player_list)], **Observers**: [length(GLOB.current_observers_list)]"
|
|
if(stealth_admins)
|
|
admin_text += "**Stealthed**: [stealth_admins]\n"
|
|
if(afk_admins)
|
|
admin_text += "**AFK**: [afk_admins]\n"
|
|
if(other_admins)
|
|
admin_text += "**Lacks +BAN**: [other_admins]\n"
|
|
embed.fields = list(
|
|
"CKEY" = initiator_ckey,
|
|
"PLAYERS" = player_count,
|
|
"ROUND STATE" = round_state,
|
|
"ROUND ID" = GLOB.round_id,
|
|
"ROUND TIME" = ROUND_TIME(),
|
|
"MESSAGE" = message,
|
|
"ADMINS" = admin_text,
|
|
)
|
|
if(CONFIG_GET(string/adminhelp_ahelp_link))
|
|
var/ahelp_link = replacetext(CONFIG_GET(string/adminhelp_ahelp_link), "$RID", GLOB.round_id)
|
|
ahelp_link = replacetext(ahelp_link, "$TID", id)
|
|
embed.url = ahelp_link
|
|
return embed
|
|
|
|
/datum/admin_help/proc/send_message_to_tgs(message, urgent = FALSE)
|
|
var/message_to_send = message
|
|
|
|
if(urgent)
|
|
var/extra_message = CONFIG_GET(string/urgent_ahelp_message)
|
|
to_chat(initiator, span_boldwarning("Notified admins to prioritize your ticket"))
|
|
var/datum/discord_embed/embed = format_embed_discord(message)
|
|
embed.content = extra_message
|
|
embed.footer = "This player requested an admin"
|
|
send2adminchat_webhook(embed, urgent = TRUE)
|
|
webhook_sent = WEBHOOK_URGENT
|
|
//send it to TGS if nobody is on and tell us how many were on
|
|
var/admin_number_present = send2tgs_adminless_only(initiator_ckey, "Ticket #[id]: [message_to_send]")
|
|
// BUBBER EDIT BEGIN
|
|
var/list/adm = get_admin_counts(R_BAN)
|
|
var/list/afkmins = adm["afk"]
|
|
var/list/stealthmins = adm["stealth"]
|
|
var/list/powerlessmins = adm["noflags"]
|
|
var/list/allmins = adm["total"]
|
|
send2adminchat("Ticket #[id]","[message_to_send] || Online Admins : \[[english_list(allmins)]\] : All admins stealthed\[[english_list(stealthmins)]\], AFK\[[english_list(afkmins)]\], or lacks +BAN\[[english_list(powerlessmins)]\]!") // BUBBER EDIT ADDITION
|
|
// BUBBER EDIT END
|
|
log_admin_private("Ticket #[id]: [key_name(initiator)]: [name] - heard by [admin_number_present] non-AFK admins who have +BAN.")
|
|
if(admin_number_present <= 0)
|
|
to_chat(initiator, span_notice("No active admins are online, your adminhelp was sent to admins who are available through IRC or Discord."), confidential = TRUE)
|
|
heard_by_no_admins = TRUE
|
|
var/regular_webhook_url = CONFIG_GET(string/regular_adminhelp_webhook_url)
|
|
if(regular_webhook_url && (!urgent || regular_webhook_url != CONFIG_GET(string/urgent_adminhelp_webhook_url)))
|
|
var/extra_message = CONFIG_GET(string/ahelp_message)
|
|
var/datum/discord_embed/embed = format_embed_discord(message)
|
|
embed.content = extra_message
|
|
embed.footer = "This player sent an ahelp when no admins are available [urgent? "and also requested an admin": ""]"
|
|
send2adminchat_webhook(embed, urgent = FALSE)
|
|
webhook_sent = WEBHOOK_NON_URGENT
|
|
|
|
/proc/send2adminchat_webhook(message_or_embed, urgent)
|
|
var/webhook = CONFIG_GET(string/urgent_adminhelp_webhook_url)
|
|
if(!urgent)
|
|
webhook = CONFIG_GET(string/regular_adminhelp_webhook_url)
|
|
|
|
if(!webhook)
|
|
return
|
|
var/list/webhook_info = list()
|
|
if(istext(message_or_embed))
|
|
var/message_content = replacetext(replacetext(message_or_embed, "\proper", ""), "\improper", "")
|
|
message_content = GLOB.has_discord_embeddable_links.Replace(replacetext(message_content, "`", ""), " ```$1``` ")
|
|
webhook_info["content"] = message_content
|
|
else
|
|
var/datum/discord_embed/embed = message_or_embed
|
|
webhook_info["embeds"] = list(embed.convert_to_list())
|
|
if(embed.content)
|
|
webhook_info["content"] = embed.content
|
|
if(CONFIG_GET(string/adminhelp_webhook_name))
|
|
webhook_info["username"] = CONFIG_GET(string/adminhelp_webhook_name)
|
|
if(CONFIG_GET(string/adminhelp_webhook_pfp))
|
|
webhook_info["avatar_url"] = CONFIG_GET(string/adminhelp_webhook_pfp)
|
|
// Uncomment when servers are moved to TGS4
|
|
// send2chat(new /datum/tgs_message_conent("[initiator_ckey] | [message_content]"), "ahelp", TRUE)
|
|
var/list/headers = list()
|
|
headers["Content-Type"] = "application/json"
|
|
var/datum/http_request/request = new()
|
|
request.prepare(RUSTG_HTTP_METHOD_POST, webhook, json_encode(webhook_info), headers, "tmp/response.json")
|
|
request.begin_async()
|
|
|
|
/datum/admin_help/Destroy()
|
|
RemoveActive()
|
|
GLOB.ahelp_tickets.closed_tickets -= src
|
|
GLOB.ahelp_tickets.resolved_tickets -= src
|
|
return ..()
|
|
|
|
/datum/admin_help/proc/AddInteraction(formatted_message, player_message)
|
|
if (!isnull(usr) && usr.ckey != initiator_ckey)
|
|
admins_involved |= usr.ckey
|
|
if(heard_by_no_admins)
|
|
heard_by_no_admins = FALSE
|
|
send2adminchat(initiator_ckey, "Ticket #[id]: Answered by [key_name(usr)]")
|
|
|
|
ticket_interactions += "[time_stamp()]: [formatted_message]"
|
|
if (!isnull(player_message))
|
|
player_interactions += "[time_stamp()]: [player_message]"
|
|
|
|
//Removes the ahelp verb and returns it after 2 minutes
|
|
/datum/admin_help/proc/TimeoutVerb()
|
|
remove_verb(initiator, /client/verb/adminhelp)
|
|
initiator.adminhelptimerid = addtimer(CALLBACK(initiator, TYPE_PROC_REF(/client, giveadminhelpverb)), 1200, TIMER_STOPPABLE) //2 minute cooldown of admin helps
|
|
|
|
//private
|
|
/datum/admin_help/proc/FullMonty(ref_src)
|
|
if(!ref_src)
|
|
ref_src = "[REF(src)]"
|
|
. = ADMIN_FULLMONTY_NONAME(initiator.mob)
|
|
. += " (<A href='byond://?_src_=holder;[HrefToken()];showmessageckey=[initiator.ckey]'>NOTES</A>)"
|
|
if(state == AHELP_ACTIVE)
|
|
if (CONFIG_GET(flag/popup_admin_pm))
|
|
. += " (<A href='byond://?_src_=holder;[HrefToken(forceGlobal = TRUE)];adminpopup=[REF(initiator)]'>POPUP</A>)"
|
|
. += ClosureLinks(ref_src)
|
|
|
|
//private
|
|
/datum/admin_help/proc/ClosureLinks(ref_src)
|
|
if(!ref_src)
|
|
ref_src = "[REF(src)]"
|
|
. = " (<A href='byond://?_src_=holder;[HrefToken(forceGlobal = TRUE)];ahelp=[ref_src];ahelp_action=reject'>REJT</A>)"
|
|
. += " (<A href='byond://?_src_=holder;[HrefToken(forceGlobal = TRUE)];ahelp=[ref_src];ahelp_action=icissue'>IC</A>)"
|
|
. += " (<A href='byond://?_src_=holder;[HrefToken(forceGlobal = TRUE)];ahelp=[ref_src];ahelp_action=close'>CLOSE</A>)"
|
|
. += " (<A href='byond://?_src_=holder;[HrefToken(forceGlobal = TRUE)];ahelp=[ref_src];ahelp_action=resolve'>RSLVE</A>)"
|
|
|
|
//private
|
|
/datum/admin_help/proc/LinkedReplyName(ref_src)
|
|
if(!ref_src)
|
|
ref_src = "[REF(src)]"
|
|
return "<A href='byond://?_src_=holder;[HrefToken(forceGlobal = TRUE)];ahelp=[ref_src];ahelp_action=reply'>[initiator_key_name]</A>"
|
|
|
|
//private
|
|
/datum/admin_help/proc/TicketHref(msg, ref_src, action = "ticket")
|
|
if(!ref_src)
|
|
ref_src = "[REF(src)]"
|
|
return "<A href='byond://?_src_=holder;[HrefToken(forceGlobal = TRUE)];ahelp=[ref_src];ahelp_action=[action]'>[msg]</A>"
|
|
|
|
//message from the initiator without a target, all admins will see this
|
|
//won't bug irc/discord
|
|
/datum/admin_help/proc/MessageNoRecipient(msg, urgent = FALSE)
|
|
msg = sanitize(copytext_char(msg, 1, MAX_MESSAGE_LEN))
|
|
var/ref_src = "[REF(src)]"
|
|
//Message to be sent to all admins
|
|
var/admin_msg = fieldset_block(
|
|
span_adminhelp("Ticket [TicketHref("#[id]", ref_src)]"),
|
|
"<b>[LinkedReplyName(ref_src)]</b>\n\n\
|
|
[span_linkify(keywords_lookup(msg))]\n\n\
|
|
<b class='smaller'>[FullMonty(ref_src)]</b>",
|
|
"boxed_message red_box")
|
|
|
|
AddInteraction("<font color='red'>[LinkedReplyName(ref_src)]: [msg]</font>", player_message = "<font color='red'>[LinkedReplyName(ref_src)]: [msg]</font>")
|
|
log_admin_private("Ticket #[id]: [key_name(initiator)]: [msg]")
|
|
|
|
//send this msg to all admins
|
|
for(var/client/X in GLOB.admins)
|
|
if(X.prefs.toggles & SOUND_ADMINHELP)
|
|
SEND_SOUND(X, sound('sound/effects/adminhelp.ogg'))
|
|
window_flash(X, ignorepref = TRUE)
|
|
to_chat(X,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = admin_msg,
|
|
confidential = TRUE)
|
|
|
|
//show it to the person adminhelping too
|
|
reply_to_admins_notification(msg)
|
|
SSblackbox.LogAhelp(id, "Ticket Opened", msg, null, initiator.ckey, urgent = urgent)
|
|
|
|
/// Sends a message to the player that they are replying to admins.
|
|
/datum/admin_help/proc/reply_to_admins_notification(message)
|
|
to_chat(
|
|
initiator,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_notice("PM to-<b>Admins</b>: [span_linkify(message)]"),
|
|
confidential = TRUE,
|
|
)
|
|
|
|
player_replied = TRUE
|
|
|
|
//Reopen a closed ticket
|
|
/datum/admin_help/proc/Reopen()
|
|
if(state == AHELP_ACTIVE)
|
|
to_chat(usr, span_warning("This ticket is already open."), confidential = TRUE)
|
|
return
|
|
|
|
if(GLOB.ahelp_tickets.CKey2ActiveTicket(initiator_ckey))
|
|
to_chat(usr, span_warning("This user already has an active ticket, cannot reopen this one."), confidential = TRUE)
|
|
return
|
|
|
|
statclick = new(null, src)
|
|
GLOB.ahelp_tickets.active_tickets += src
|
|
GLOB.ahelp_tickets.closed_tickets -= src
|
|
GLOB.ahelp_tickets.resolved_tickets -= src
|
|
switch(state)
|
|
if(AHELP_CLOSED)
|
|
SSblackbox.record_feedback("tally", "ahelp_stats", -1, "closed")
|
|
if(AHELP_RESOLVED)
|
|
SSblackbox.record_feedback("tally", "ahelp_stats", -1, "resolved")
|
|
state = AHELP_ACTIVE
|
|
closed_at = null
|
|
if(initiator)
|
|
initiator.current_ticket = src
|
|
send2adminchat("Ticket #[id]:", "Reopened!") // BUBBER EDIT - LOG ALL AHELPS TO DISCORD
|
|
AddInteraction("<font color='purple'>Reopened by [key_name_admin(usr)]</font>", player_message = "Ticket reopened!")
|
|
var/msg = span_adminhelp("Ticket [TicketHref("#[id]")] reopened by [key_name_admin(usr)].")
|
|
message_admins(msg)
|
|
log_admin_private(msg)
|
|
SSblackbox.LogAhelp(id, "Reopened", "Reopened by [usr.key]", usr.ckey)
|
|
SSblackbox.record_feedback("tally", "ahelp_stats", 1, "reopened")
|
|
TicketPanel() //can only be done from here, so refresh it
|
|
|
|
//private
|
|
/datum/admin_help/proc/RemoveActive()
|
|
if(state != AHELP_ACTIVE)
|
|
return
|
|
closed_at = world.time
|
|
QDEL_NULL(statclick)
|
|
GLOB.ahelp_tickets.active_tickets -= src
|
|
if(initiator && initiator.current_ticket == src)
|
|
initiator.current_ticket = null
|
|
|
|
SEND_SIGNAL(src, COMSIG_ADMIN_HELP_MADE_INACTIVE)
|
|
|
|
//Mark open ticket as closed/meme
|
|
/datum/admin_help/proc/Close(key_name = key_name_admin(usr), silent = FALSE)
|
|
if(state != AHELP_ACTIVE)
|
|
return
|
|
//SKYRAT EDIT ADDITION BEGIN - ADMIN
|
|
if(handler && handler != usr.ckey)
|
|
var/response = tgui_alert(usr, "This ticket is already being handled by [handler]. Do you want to continue?", "Ticket already assigned", list("Yes", "No"))
|
|
if(!response || response == "No")
|
|
return
|
|
//SKYRAT EDIT ADDITION END
|
|
RemoveActive()
|
|
state = AHELP_CLOSED
|
|
GLOB.ahelp_tickets.ListInsert(src)
|
|
AddInteraction("<font color='red'>Closed by [key_name].</font>", player_message = "<font color='red'>Ticket closed!</font>")
|
|
send2adminchat("Ticket #[id]:", "Marked as closed by [key_name]") // BUBBER EDIT - LOG ALL AHELPS TO DISCORD
|
|
if(!silent)
|
|
SSblackbox.record_feedback("tally", "ahelp_stats", 1, "closed")
|
|
var/msg = "Ticket [TicketHref("#[id]")] closed by [key_name]."
|
|
message_admins(msg)
|
|
SSblackbox.LogAhelp(id, "Closed", "Closed by [usr.key]", null, usr.ckey)
|
|
log_admin_private(msg)
|
|
|
|
//Mark open ticket as resolved/legitimate, returns ahelp verb
|
|
/datum/admin_help/proc/Resolve(key_name = key_name_admin(usr), silent = FALSE)
|
|
if(state != AHELP_ACTIVE)
|
|
return
|
|
//SKYRAT EDIT ADDITION BEGIN - ADMIN
|
|
if(handler && handler != usr.ckey)
|
|
var/response = tgui_alert(usr, "This ticket is already being handled by [handler]. Do you want to continue?", "Ticket already assigned", list("Yes", "No"))
|
|
if(!response || response == "No")
|
|
return
|
|
//SKYRAT EDIT ADDITION END
|
|
RemoveActive()
|
|
state = AHELP_RESOLVED
|
|
GLOB.ahelp_tickets.ListInsert(src)
|
|
|
|
addtimer(CALLBACK(initiator, TYPE_PROC_REF(/client, giveadminhelpverb)), 5 SECONDS)
|
|
send2adminchat("Ticket #[id]:", "Marked as resolved by [key_name]") // BUBBER EDIT - LOG ALL AHELPS TO DISCORD
|
|
AddInteraction("<font color='green'>Resolved by [key_name].</font>", player_message = "<font color='green'>Ticket resolved!</font>")
|
|
to_chat(initiator, span_adminhelp("Your ticket has been resolved by an admin. The Adminhelp verb will be returned to you shortly."), confidential = TRUE)
|
|
if(!silent)
|
|
SSblackbox.record_feedback("tally", "ahelp_stats", 1, "resolved")
|
|
var/msg = "Ticket [TicketHref("#[id]")] resolved by [key_name]"
|
|
message_admins(msg)
|
|
SSblackbox.LogAhelp(id, "Resolved", "Resolved by [usr.key]", null, usr.ckey)
|
|
log_admin_private(msg)
|
|
|
|
//Close and return ahelp verb, use if ticket is incoherent
|
|
/datum/admin_help/proc/Reject(key_name = key_name_admin(usr))
|
|
if(state != AHELP_ACTIVE)
|
|
return
|
|
//SKYRAT EDIT ADDITION BEGIN - ADMIN
|
|
if(handler && handler != usr.ckey)
|
|
var/response = tgui_alert(usr, "This ticket is already being handled by [handler]. Do you want to continue?", "Ticket already assigned", list("Yes", "No"))
|
|
if(!response || response == "No")
|
|
return
|
|
//SKYRAT EDIT ADDITION END
|
|
if(initiator)
|
|
initiator.giveadminhelpverb()
|
|
send2adminchat("[key_name]", "Rejected Adminhelp: [id]") // BUBBER EDIT - LOG ALL AHELPS TO DISCORD
|
|
SEND_SOUND(initiator, sound('sound/effects/adminhelp.ogg'))
|
|
|
|
to_chat(initiator, "<font color='red' size='4'><b>- AdminHelp Rejected! -</b></font>", confidential = TRUE)
|
|
to_chat(initiator, "<font color='red'><b>Your admin help was rejected.</b> The adminhelp verb has been returned to you so that you may try again.</font>", confidential = TRUE)
|
|
to_chat(initiator, "Please try to be calm, clear, and descriptive in admin helps, do not assume the admin has seen any related events, and clearly state the names of anybody you are reporting.", confidential = TRUE)
|
|
|
|
SSblackbox.record_feedback("tally", "ahelp_stats", 1, "rejected")
|
|
var/msg = "Ticket [TicketHref("#[id]")] rejected by [key_name]"
|
|
message_admins(msg)
|
|
log_admin_private(msg)
|
|
AddInteraction("Rejected by [key_name].", player_message = "Ticket rejected!")
|
|
SSblackbox.LogAhelp(id, "Rejected", "Rejected by [usr.key]", null, usr.ckey)
|
|
Close(silent = TRUE)
|
|
|
|
//Resolve ticket with IC Issue message
|
|
/datum/admin_help/proc/ICIssue(key_name = key_name_admin(usr))
|
|
if(state != AHELP_ACTIVE)
|
|
return
|
|
//SKYRAT EDIT ADDITION BEGIN - ADMIN
|
|
if(handler && handler != usr.ckey)
|
|
var/response = tgui_alert(usr, "This ticket is already being handled by [handler]. Do you want to continue?", "Ticket already assigned", list("Yes", "No"))
|
|
if(!response || response == "No")
|
|
return
|
|
//SKYRAT EDIT ADDITION END
|
|
var/msg = "<font color='red' size='4'><b>- AdminHelp marked as IC issue! -</b></font><br>"
|
|
msg += "<font color='red'>Your issue has been determined by an administrator to be an in character issue and does NOT require administrator intervention at this time. For further resolution you should pursue options that are in character.</font>"
|
|
|
|
if(initiator)
|
|
to_chat(initiator, msg, confidential = TRUE)
|
|
send2adminchat("Ticket #[id]:", "Marked as IC issue by [key_name]") // BUBBER EDIT - LOG ALL AHELPS TO DISCORD
|
|
SSblackbox.record_feedback("tally", "ahelp_stats", 1, "IC")
|
|
msg = "Ticket [TicketHref("#[id]")] marked as IC by [key_name]"
|
|
message_admins(msg)
|
|
log_admin_private(msg)
|
|
AddInteraction("Marked as IC issue by [key_name]", player_message = "Marked as IC issue!")
|
|
SSblackbox.LogAhelp(id, "IC Issue", "Marked as IC issue by [usr.key]", null, usr.ckey)
|
|
Resolve(silent = TRUE)
|
|
|
|
//Show the ticket panel
|
|
/datum/admin_help/proc/TicketPanel()
|
|
var/list/dat = list("<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Ticket #[id]</title></head>")
|
|
var/ref_src = "[REF(src)]"
|
|
dat += "<h4>Admin Help Ticket #[id]: [LinkedReplyName(ref_src)]</h4>"
|
|
dat += "<b>State: [ticket_status()]</b>"
|
|
dat += "[FOURSPACES][TicketHref("Refresh", ref_src)][FOURSPACES][TicketHref("Re-Title", ref_src, "retitle")]"
|
|
if(state != AHELP_ACTIVE)
|
|
dat += "[FOURSPACES][TicketHref("Reopen", ref_src, "reopen")]"
|
|
dat += "<br><br>Opened at: [gameTimestamp(wtime = opened_at)] (Approx [DisplayTimeText(world.time - opened_at)] ago)"
|
|
if(closed_at)
|
|
dat += "<br>Closed at: [gameTimestamp(wtime = closed_at)] (Approx [DisplayTimeText(world.time - closed_at)] ago)"
|
|
dat += "<br><br>"
|
|
if(initiator)
|
|
dat += "<b>Actions:</b> [FullMonty(ref_src)]<br>"
|
|
else
|
|
dat += "<b>DISCONNECTED</b>[FOURSPACES][ClosureLinks(ref_src)]<br>"
|
|
dat += "<br><b>Log:</b><br><br>"
|
|
for(var/I in ticket_interactions)
|
|
dat += "[I]<br>"
|
|
|
|
// Helper for opening directly to player ticket history
|
|
dat += "<br><br><b>Player Ticket History:</b>"
|
|
dat += "[FOURSPACES]<A href='byond://?_src_=holder;[HrefToken()];player_ticket_history=[initiator_ckey]'>Open</A>"
|
|
|
|
// Append any tickets also opened by this user if relevant
|
|
var/list/related_tickets = GLOB.ahelp_tickets.TicketsByCKey(initiator_ckey)
|
|
if (related_tickets.len > 1)
|
|
dat += "<br/><b>Other Tickets by User</b><br/>"
|
|
for (var/datum/admin_help/related_ticket in related_tickets)
|
|
if (related_ticket.id == id)
|
|
continue
|
|
dat += "[related_ticket.TicketHref("#[related_ticket.id]")] ([related_ticket.ticket_status()]): [related_ticket.name]<br/>"
|
|
dat += "</html>"
|
|
|
|
usr << browse(dat.Join(), "window=ahelp[id];size=750x480")
|
|
|
|
/**
|
|
* Renders the current status of the ticket into a displayable string
|
|
*/
|
|
/datum/admin_help/proc/ticket_status()
|
|
switch(state)
|
|
if(AHELP_ACTIVE)
|
|
return "<font color='red'>OPEN</font>"
|
|
if(AHELP_RESOLVED)
|
|
return "<font color='green'>RESOLVED</font>"
|
|
if(AHELP_CLOSED)
|
|
return "CLOSED"
|
|
else
|
|
stack_trace("Invalid ticket state: [state]")
|
|
return "INVALID, CALL A CODER"
|
|
|
|
/datum/admin_help/proc/Retitle()
|
|
var/new_title = input(usr, "Enter a title for the ticket", "Rename Ticket", name) as text|null
|
|
if(new_title)
|
|
name = new_title
|
|
//not saying the original name cause it could be a long ass message
|
|
var/msg = "Ticket [TicketHref("#[id]")] titled [name] by [key_name_admin(usr)]"
|
|
message_admins(msg)
|
|
log_admin_private(msg)
|
|
TicketPanel() //we have to be here to do this
|
|
|
|
//Forwarded action from admin/Topic
|
|
/datum/admin_help/proc/Action(action)
|
|
testing("Ahelp action: [action]")
|
|
if(webhook_sent != WEBHOOK_NONE)
|
|
var/datum/discord_embed/embed = new()
|
|
embed.title = "Ticket #[id]"
|
|
if(CONFIG_GET(string/adminhelp_ahelp_link))
|
|
var/ahelp_link = replacetext(CONFIG_GET(string/adminhelp_ahelp_link), "$RID", GLOB.round_id)
|
|
ahelp_link = replacetext(ahelp_link, "$TID", id)
|
|
embed.url = ahelp_link
|
|
embed.description = "[key_name(usr)] has sent an action to this ticket. Action ID: [action]"
|
|
if(webhook_sent == WEBHOOK_URGENT)
|
|
send2adminchat_webhook(embed, urgent = TRUE)
|
|
if(webhook_sent == WEBHOOK_NON_URGENT || CONFIG_GET(string/regular_adminhelp_webhook_url) != CONFIG_GET(string/urgent_adminhelp_webhook_url))
|
|
send2adminchat_webhook(embed, urgent = FALSE)
|
|
webhook_sent = WEBHOOK_NONE
|
|
switch(action)
|
|
if("ticket")
|
|
TicketPanel()
|
|
if("retitle")
|
|
Retitle()
|
|
if("reject")
|
|
Reject()
|
|
if("reply")
|
|
// SKYRAT EDIT START - ADMIN HANDLE
|
|
if(handle_issue())
|
|
usr?.client.cmd_ahelp_reply(initiator)
|
|
// SKYRAT EDIT END
|
|
if("icissue")
|
|
ICIssue()
|
|
if("close")
|
|
Close()
|
|
if("resolve")
|
|
Resolve()
|
|
if("reopen")
|
|
Reopen()
|
|
// SKYRAT EDIT ADDITION
|
|
if("handle_issue")
|
|
handle_issue()
|
|
if("pingmute")
|
|
ticket_ping_stop = !ticket_ping_stop
|
|
SSblackbox.record_feedback("tally", "ahelp_stats", 1, "pingmute")
|
|
var/msg = "Ticket [TicketHref("#[id]")] has been [ticket_ping_stop ? "" : "un"]muted from the Ticket Ping Subsystem by [key_name_admin(usr)]."
|
|
message_admins(msg)
|
|
log_admin_private(msg)
|
|
if("convert")
|
|
convert_to_mentorhelp()
|
|
// SKYRAT EDIT ADDITION END
|
|
|
|
/datum/admin_help/proc/player_ticket_panel()
|
|
var/list/dat = list("<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Player Ticket</title></head>")
|
|
dat += "<b>State: "
|
|
switch(state)
|
|
if(AHELP_ACTIVE)
|
|
dat += "<font color='red'>OPEN</font></b>"
|
|
if(AHELP_RESOLVED)
|
|
dat += "<font color='green'>RESOLVED</font></b>"
|
|
if(AHELP_CLOSED)
|
|
dat += "CLOSED</b>"
|
|
else
|
|
dat += "UNKNOWN</b>"
|
|
dat += "\n[FOURSPACES]<A href='byond://?_src_=holder;[HrefToken(forceGlobal = TRUE)];player_ticket_panel=1'>Refresh</A>"
|
|
dat += "<br><br>Opened at: [gameTimestamp("hh:mm:ss", opened_at)] (Approx [DisplayTimeText(world.time - opened_at)] ago)"
|
|
if(closed_at)
|
|
dat += "<br>Closed at: [gameTimestamp("hh:mm:ss", closed_at)] (Approx [DisplayTimeText(world.time - closed_at)] ago)"
|
|
dat += "<br><br>"
|
|
dat += "<br><b>Log:</b><br><br>"
|
|
for (var/interaction in player_interactions)
|
|
dat += "[interaction]<br>"
|
|
|
|
var/datum/browser/player_panel = new(usr, "ahelp[id]", 0, 620, 480)
|
|
player_panel.set_content(dat.Join())
|
|
player_panel.open()
|
|
|
|
|
|
//
|
|
// TICKET STATCLICK
|
|
//
|
|
|
|
/obj/effect/statclick/ahelp
|
|
var/datum/admin_help/ahelp_datum
|
|
|
|
/obj/effect/statclick/ahelp/Initialize(mapload, datum/admin_help/AH)
|
|
ahelp_datum = AH
|
|
. = ..()
|
|
|
|
/obj/effect/statclick/ahelp/update()
|
|
return ..(ahelp_datum.name)
|
|
|
|
/obj/effect/statclick/ahelp/Click()
|
|
if (!usr.client?.holder)
|
|
message_admins("[key_name_admin(usr)] non-holder clicked on an ahelp statclick! ([src])")
|
|
usr.log_message("non-holder clicked on an ahelp statclick! ([src])", LOG_ADMIN)
|
|
return
|
|
|
|
ahelp_datum.TicketPanel()
|
|
|
|
/obj/effect/statclick/ahelp/Destroy()
|
|
ahelp_datum = null
|
|
return ..()
|
|
|
|
//
|
|
// CLIENT PROCS
|
|
//
|
|
|
|
/client/proc/giveadminhelpverb()
|
|
add_verb(src, /client/verb/adminhelp)
|
|
deltimer(adminhelptimerid)
|
|
adminhelptimerid = 0
|
|
|
|
GLOBAL_DATUM_INIT(admin_help_ui_handler, /datum/admin_help_ui_handler, new)
|
|
|
|
/datum/admin_help_ui_handler
|
|
var/list/ahelp_cooldowns = list()
|
|
|
|
/datum/admin_help_ui_handler/ui_state(mob/user)
|
|
return GLOB.always_state
|
|
|
|
/datum/admin_help_ui_handler/ui_data(mob/user)
|
|
. = list()
|
|
var/list/admins = get_admin_counts(R_BAN)
|
|
.["adminCount"] = length(admins["present"])
|
|
|
|
/datum/admin_help_ui_handler/ui_static_data(mob/user)
|
|
. = list()
|
|
.["bannedFromUrgentAhelp"] = is_banned_from(user.ckey, "Urgent Adminhelp")
|
|
.["urgentAhelpPromptMessage"] = CONFIG_GET(string/urgent_ahelp_user_prompt)
|
|
var/webhook_url = CONFIG_GET(string/urgent_adminhelp_webhook_url)
|
|
if(webhook_url)
|
|
.["urgentAhelpEnabled"] = TRUE
|
|
|
|
/datum/admin_help_ui_handler/ui_interact(mob/user, datum/tgui/ui)
|
|
ui = SStgui.try_update_ui(user, src, ui)
|
|
if(!ui)
|
|
ui = new(user, src, "Adminhelp")
|
|
ui.open()
|
|
ui.set_autoupdate(FALSE)
|
|
|
|
/datum/admin_help_ui_handler/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
|
|
. = ..()
|
|
if(.)
|
|
return
|
|
var/client/user_client = usr.client
|
|
var/message = sanitize_text(trim(params["message"]))
|
|
var/urgent = !!params["urgent"]
|
|
var/list/admins = get_admin_counts(R_BAN)
|
|
if(length(admins["present"]) != 0 || is_banned_from(user_client.ckey, "Urgent Adminhelp"))
|
|
urgent = FALSE
|
|
|
|
if(user_client.adminhelptimerid)
|
|
return
|
|
|
|
perform_adminhelp(user_client, message, urgent)
|
|
ui.close()
|
|
|
|
/datum/admin_help_ui_handler/proc/perform_adminhelp(client/user_client, message, urgent)
|
|
if(GLOB.say_disabled) //This is here to try to identify lag problems
|
|
to_chat(usr, span_danger("Speech is currently admin-disabled."), confidential = TRUE)
|
|
return
|
|
|
|
if(!message)
|
|
return
|
|
|
|
//handle muting and automuting
|
|
if(user_client.prefs.muted & MUTE_ADMINHELP)
|
|
to_chat(user_client, span_danger("Error: Admin-PM: You cannot send adminhelps (Muted)."), confidential = TRUE)
|
|
return
|
|
if(user_client.handle_spam_prevention(message, MUTE_ADMINHELP))
|
|
return
|
|
|
|
BLACKBOX_LOG_ADMIN_VERB("Adminhelp")
|
|
|
|
if(urgent)
|
|
if(!COOLDOWN_FINISHED(src, ahelp_cooldowns?[user_client.ckey]))
|
|
urgent = FALSE // Prevent abuse
|
|
else
|
|
COOLDOWN_START(src, ahelp_cooldowns[user_client.ckey], CONFIG_GET(number/urgent_ahelp_cooldown) * (1 SECONDS))
|
|
|
|
if(user_client.current_ticket)
|
|
user_client.current_ticket.TimeoutVerb()
|
|
if(urgent)
|
|
var/sanitized_message = sanitize(copytext_char(message, 1, MAX_MESSAGE_LEN))
|
|
user_client.current_ticket.send_message_to_tgs(sanitized_message, urgent = TRUE)
|
|
user_client.current_ticket.MessageNoRecipient(message, urgent)
|
|
return
|
|
|
|
new /datum/admin_help(message, user_client, FALSE, null, urgent) // SKYRAT EDIT - Handling tickets - ORIGINAL: new /datum/admin_help(message, user_client, FALSE, urgent)
|
|
|
|
/client/verb/no_tgui_adminhelp(message as message)
|
|
set name = "NoTguiAdminhelp"
|
|
set hidden = TRUE
|
|
|
|
if(adminhelptimerid)
|
|
return
|
|
|
|
message = trim(message)
|
|
|
|
GLOB.admin_help_ui_handler.perform_adminhelp(src, message, FALSE)
|
|
|
|
/client/verb/adminhelp()
|
|
set category = "Admin"
|
|
set name = "Adminhelp"
|
|
GLOB.admin_help_ui_handler.ui_interact(mob)
|
|
to_chat(src, span_boldnotice("Adminhelp failing to open or work? <a href='byond://?src=[REF(src)];tguiless_adminhelp=1'>Click here</a>"))
|
|
|
|
/client/verb/view_latest_ticket()
|
|
set category = "Admin"
|
|
set name = "View Latest Ticket"
|
|
|
|
if(!current_ticket)
|
|
// Check if the client had previous tickets, and show the latest one
|
|
var/list/prev_tickets = list()
|
|
var/datum/admin_help/last_ticket
|
|
// Check all resolved tickets for this player
|
|
for(var/datum/admin_help/resolved_ticket in GLOB.ahelp_tickets.resolved_tickets)
|
|
if(resolved_ticket.initiator_ckey == ckey) // Initiator is a misnomer, it's always the non-admin player even if an admin bwoinks first
|
|
prev_tickets += resolved_ticket
|
|
// Check all closed tickets for this player
|
|
for(var/datum/admin_help/closed_ticket in GLOB.ahelp_tickets.closed_tickets)
|
|
if(closed_ticket.initiator_ckey == ckey)
|
|
prev_tickets += closed_ticket
|
|
// Take the most recent entry of prev_tickets and open the panel on it
|
|
if(LAZYLEN(prev_tickets))
|
|
last_ticket = pop(prev_tickets)
|
|
last_ticket.player_ticket_panel()
|
|
return
|
|
|
|
// client had no tickets this round
|
|
to_chat(src, span_warning("You have not had an ahelp ticket this round."))
|
|
return
|
|
|
|
current_ticket.player_ticket_panel()
|
|
|
|
//
|
|
// LOGGING
|
|
//
|
|
|
|
/// Use this proc when an admin takes action that may be related to an open ticket on what
|
|
/// what can be a client, ckey, or mob
|
|
/// player_message: If the message should be shown in the player ticket panel, fill this out
|
|
/// log_in_blackbox: Whether or not this message with the blackbox system.
|
|
/// If disabled, this message should be logged with a different proc call
|
|
/proc/admin_ticket_log(what, message, player_message, log_in_blackbox = TRUE)
|
|
var/client/mob_client
|
|
var/mob/Mob = what
|
|
if(istype(Mob))
|
|
mob_client = Mob.client
|
|
else
|
|
mob_client = what
|
|
if(istype(mob_client) && mob_client.current_ticket)
|
|
if (isnull(player_message))
|
|
mob_client.current_ticket.AddInteraction(message)
|
|
else
|
|
mob_client.current_ticket.AddInteraction(message, player_message)
|
|
if(log_in_blackbox)
|
|
SSblackbox.LogAhelp(mob_client.current_ticket.id, "Interaction", message, mob_client.ckey, usr.ckey)
|
|
return mob_client.current_ticket
|
|
if(istext(what)) //ckey
|
|
send2adminchat("[what]", message) // BUBBER EDIT - LOG ALL AHELPS TO DISCORD
|
|
var/datum/admin_help/active_admin_help = GLOB.ahelp_tickets.CKey2ActiveTicket(what)
|
|
if(active_admin_help)
|
|
if (isnull(player_message))
|
|
active_admin_help.AddInteraction(message)
|
|
else
|
|
active_admin_help.AddInteraction(message, player_message)
|
|
if(log_in_blackbox)
|
|
SSblackbox.LogAhelp(active_admin_help.id, "Interaction", message, what, usr.ckey)
|
|
return active_admin_help
|
|
|
|
//
|
|
// HELPER PROCS
|
|
//
|
|
|
|
/proc/get_admin_counts(requiredflags = R_BAN)
|
|
. = list("total" = list(), "noflags" = list(), "afk" = list(), "stealth" = list(), "present" = list())
|
|
for(var/client/X in GLOB.admins)
|
|
.["total"] += X
|
|
if(requiredflags != NONE && !check_rights_for(X, requiredflags))
|
|
.["noflags"] += X
|
|
else if(X.is_afk())
|
|
.["afk"] += X
|
|
else if(X.holder.fakekey)
|
|
.["stealth"] += X
|
|
else
|
|
.["present"] += X
|
|
|
|
/proc/send2tgs_adminless_only(source, msg, requiredflags = R_BAN)
|
|
var/list/adm = get_admin_counts(requiredflags)
|
|
var/list/activemins = adm["present"]
|
|
. = activemins.len
|
|
if(. <= 0)
|
|
var/final = ""
|
|
var/list/afkmins = adm["afk"]
|
|
var/list/stealthmins = adm["stealth"]
|
|
var/list/powerlessmins = adm["noflags"]
|
|
var/list/allmins = adm["total"]
|
|
if(!afkmins.len && !stealthmins.len && !powerlessmins.len)
|
|
final = "[msg] - No admins online"
|
|
else
|
|
final = "[msg] - All admins stealthed\[[english_list(stealthmins)]\], AFK\[[english_list(afkmins)]\], or lacks +BAN\[[english_list(powerlessmins)]\]! Total: [allmins.len] "
|
|
send2adminchat(source,final)
|
|
send2otherserver(source,final)
|
|
|
|
/**
|
|
* Sends a message to a set of cross-communications-enabled servers using world topic calls
|
|
*
|
|
* Arguments:
|
|
* * source - Who sent this message
|
|
* * msg - The message body
|
|
* * type - The type of message, becomes the topic command under the hood
|
|
* * target_servers - A collection of servers to send the message to, defined in config
|
|
* * additional_data - An (optional) associated list of extra parameters and data to send with this world topic call
|
|
*/
|
|
/proc/send2otherserver(source, msg, type = "Ahelp", target_servers, list/additional_data = list())
|
|
if(!CONFIG_GET(string/comms_key))
|
|
debug_world_log("Server cross-comms message not sent for lack of configured key")
|
|
return
|
|
|
|
var/our_id = CONFIG_GET(string/cross_comms_name)
|
|
additional_data["message_sender"] = source
|
|
additional_data["message"] = msg
|
|
additional_data["source"] = "([our_id])"
|
|
additional_data += type
|
|
|
|
var/list/servers = CONFIG_GET(keyed_list/cross_server)
|
|
for(var/I in servers)
|
|
if(I == our_id) //No sending to ourselves
|
|
continue
|
|
if(target_servers && !(I in target_servers))
|
|
continue
|
|
world.send_cross_comms(I, additional_data)
|
|
|
|
/// Sends a message to a given cross comms server by name (by name for security).
|
|
/world/proc/send_cross_comms(server_name, list/message, auth = TRUE)
|
|
set waitfor = FALSE
|
|
if (auth)
|
|
var/comms_key = CONFIG_GET(string/comms_key)
|
|
if(!comms_key)
|
|
debug_world_log("Server cross-comms message not sent for lack of configured key")
|
|
return
|
|
message["key"] = comms_key
|
|
var/list/servers = CONFIG_GET(keyed_list/cross_server)
|
|
var/server_url = servers[server_name]
|
|
if (!server_url)
|
|
CRASH("Invalid cross comms config: [server_name]")
|
|
world.Export("[server_url]?[list2params(message)]")
|
|
|
|
|
|
/proc/tgsadminwho()
|
|
var/list/message = list("Admins: ")
|
|
var/list/admin_keys = list()
|
|
for(var/adm in GLOB.admins)
|
|
var/client/C = adm
|
|
admin_keys += "[C][C.holder.fakekey ? "(Stealth)" : ""][C.is_afk() ? "(AFK)" : ""]"
|
|
|
|
for(var/admin in admin_keys)
|
|
if(LAZYLEN(message) > 1)
|
|
message += ", [admin]"
|
|
else
|
|
message += "[admin]"
|
|
|
|
return jointext(message, "")
|
|
|
|
/proc/keywords_lookup(msg,external)
|
|
|
|
//This is a list of words which are ignored by the parser when comparing message contents for names. MUST BE IN LOWER CASE!
|
|
var/list/adminhelp_ignored_words = list("unknown","the","a","an","of","monkey","alien","as", "i")
|
|
|
|
//explode the input msg into a list
|
|
var/list/msglist = splittext(msg, " ")
|
|
|
|
//generate keywords lookup
|
|
var/list/surnames = list()
|
|
var/list/forenames = list()
|
|
var/list/ckeys = list()
|
|
var/founds = ""
|
|
for(var/mob/M in GLOB.mob_list)
|
|
var/list/indexing = list(M.real_name, M.name)
|
|
if(M.mind)
|
|
indexing += M.mind.name
|
|
|
|
for(var/string in indexing)
|
|
var/list/L = splittext(string, " ")
|
|
var/surname_found = 0
|
|
//surnames
|
|
for(var/i=L.len, i >= 1, i--)
|
|
var/word = ckey(L[i])
|
|
if(word)
|
|
surnames[word] = M
|
|
surname_found = i
|
|
break
|
|
//forenames
|
|
for(var/i in 1 to surname_found-1)
|
|
var/word = ckey(L[i])
|
|
if(word)
|
|
forenames[word] = M
|
|
//ckeys
|
|
ckeys[M.ckey] = M
|
|
|
|
var/ai_found = 0
|
|
msg = ""
|
|
var/list/mobs_found = list()
|
|
for(var/original_word in msglist)
|
|
var/word = ckey(original_word)
|
|
if(word)
|
|
if(!(word in adminhelp_ignored_words))
|
|
if(word == "ai")
|
|
ai_found = 1
|
|
else
|
|
var/mob/found = ckeys[word]
|
|
if(!found)
|
|
found = surnames[word]
|
|
if(!found)
|
|
found = forenames[word]
|
|
if(found)
|
|
if(!(found in mobs_found))
|
|
mobs_found += found
|
|
if(!ai_found && isAI(found))
|
|
ai_found = 1
|
|
var/is_antag = found.is_antag()
|
|
founds += "Name: [found.name]([found.real_name]) Key: [found.key] Ckey: [found.ckey] [is_antag ? "(Antag)" : null] "
|
|
msg += "[original_word]<font size='1' color='[is_antag ? "red" : "black"]'>(<A href='byond://?_src_=holder;[HrefToken(forceGlobal = TRUE)];adminmoreinfo=[REF(found)]'>?</A>|<A href='byond://?_src_=holder;[HrefToken(forceGlobal = TRUE)];adminplayerobservefollow=[REF(found)]'>F</A>)</font> "
|
|
continue
|
|
msg += "[original_word] "
|
|
if(external)
|
|
if(founds == "")
|
|
return "Search Failed"
|
|
else
|
|
return founds
|
|
|
|
return msg
|
|
|
|
/proc/get_mob_by_name(msg)
|
|
//This is a list of words which are ignored by the parser when comparing message contents for names. MUST BE IN LOWER CASE!
|
|
var/list/ignored_words = list("unknown","the","a","an","of","monkey","alien","as", "i")
|
|
|
|
//explode the input msg into a list
|
|
var/list/msglist = splittext(msg, " ")
|
|
|
|
//who might fit the shoe
|
|
var/list/potential_hits = list()
|
|
|
|
for(var/i in GLOB.mob_list)
|
|
var/mob/M = i
|
|
var/list/nameWords = list()
|
|
if(!M.mind)
|
|
continue
|
|
|
|
for(var/string in splittext(LOWER_TEXT(M.real_name), " "))
|
|
if(!(string in ignored_words))
|
|
nameWords += string
|
|
for(var/string in splittext(LOWER_TEXT(M.name), " "))
|
|
if(!(string in ignored_words))
|
|
nameWords += string
|
|
|
|
for(var/string in nameWords)
|
|
if(string in msglist)
|
|
potential_hits += M
|
|
break
|
|
|
|
return potential_hits
|
|
|
|
/**
|
|
* Checks a given message to see if any of the words are something we want to treat specially, as detailed below.
|
|
*
|
|
* There are 3 cases where a word is something we want to act on
|
|
* 1. Admin pings, like @adminckey. Pings the admin in question, text is not clickable
|
|
* 2. Datum refs, like @0x2001169 or @mob_23. Clicking on the link opens up the VV for that datum
|
|
* 3. Ticket refs, like #3. Displays the status and ahelper in the link, clicking on it brings up the ticket panel for it.
|
|
* Returns a list being used as a tuple. Index ASAY_LINK_NEW_MESSAGE_INDEX contains the new message text (with clickable links and such)
|
|
* while index ASAY_LINK_PINGED_ADMINS_INDEX contains a list of pinged admin clients, if there are any.
|
|
*
|
|
* Arguments:
|
|
* * msg - the message being scanned
|
|
*/
|
|
/proc/check_asay_links(msg)
|
|
var/list/msglist = splittext(msg, " ") //explode the input msg into a list
|
|
var/list/pinged_admins = list() // if we ping any admins, store them here so we can ping them after
|
|
var/modified = FALSE // did we find anything?
|
|
|
|
var/i = 0
|
|
for(var/word in msglist)
|
|
i++
|
|
if(!length(word))
|
|
continue
|
|
|
|
switch(word[1])
|
|
if("@")
|
|
var/stripped_word = ckey(copytext(word, 2))
|
|
|
|
// first we check if it's a ckey of an admin
|
|
var/client/client_check = GLOB.directory[stripped_word]
|
|
if(client_check?.holder)
|
|
msglist[i] = "<u>[word]</u>"
|
|
pinged_admins[stripped_word] = client_check
|
|
modified = TRUE
|
|
continue
|
|
|
|
// then if not, we check if it's a datum ref
|
|
|
|
var/word_with_brackets = "\[[stripped_word]\]" // the actual memory address lookups need the bracket wraps
|
|
var/datum/datum_check = locate(word_with_brackets)
|
|
if(!istype(datum_check))
|
|
continue
|
|
msglist[i] = "<u><a href='byond://?_src_=vars;[HrefToken(forceGlobal = TRUE)];Vars=[word_with_brackets]'>[word]</A></u>"
|
|
modified = TRUE
|
|
|
|
if("#") // check if we're linking a ticket
|
|
var/possible_ticket_id = text2num(copytext(word, 2))
|
|
if(!possible_ticket_id)
|
|
continue
|
|
|
|
var/datum/admin_help/ahelp_check = GLOB.ahelp_tickets?.TicketByID(possible_ticket_id)
|
|
if(!ahelp_check)
|
|
continue
|
|
|
|
var/state_word
|
|
switch(ahelp_check.state)
|
|
if(AHELP_ACTIVE)
|
|
state_word = "Active"
|
|
if(AHELP_CLOSED)
|
|
state_word = "Closed"
|
|
if(AHELP_RESOLVED)
|
|
state_word = "Resolved"
|
|
|
|
msglist[i]= "<u><A href='byond://?_src_=holder;[HrefToken(forceGlobal = TRUE)];ahelp=[REF(ahelp_check)];ahelp_action=ticket'>[word] ([state_word] | [ahelp_check.initiator_key_name])</A></u>"
|
|
modified = TRUE
|
|
|
|
if(modified)
|
|
var/list/return_list = list()
|
|
return_list[ASAY_LINK_NEW_MESSAGE_INDEX] = jointext(msglist, " ") // without tuples, we must make do!
|
|
return_list[ASAY_LINK_PINGED_ADMINS_INDEX] = pinged_admins
|
|
return return_list
|
|
|
|
|
|
#undef WEBHOOK_URGENT
|
|
#undef WEBHOOK_NONE
|
|
#undef WEBHOOK_NON_URGENT
|