mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-06-17 12:13:49 +01:00
7aa6664021
* Fix Conflicts * Change COGBAR_ANIMATION_TIME to seconds and not deciseconds (#82530) Most people should not be using this define * New Battle Arcade (#81810) Remakes Battle Arcade from just about the ground up, with exceptions taken for emagged stuff since I didn't really want to touch its behavior. The Battle Arcade now has stages that players can go through, unlocking a stage by beating 2 enemies and the boss of the previous one, but this must all be done in a row. You can choose to take a break between each battle and there's a good chance you'll sleep just fine but there's also a chance it can go wrong either through an ambush or robbery. The Inn lets you restore everything for 15 gold and you can buy a sword and armor, each level you unlock is a new sword and armor pair you can buy that's better than the last, it's 30 gold each but scales up as you progress through levels. They are really worth getting so it's best to try to not lose your money early in. The battle system is nearly the same as how it was before but I removed the poor combo system that plagued the old arcade as one big knowledge lock, now it's more just turn based. The game is built on permadeath so dying means you restart from the beginning, but if you are going to lose you can try to escape instead which costs you half of your gold. Getting to higher levels increases the difficulty of enemies but also increases the gaming exp rewards which could make this a better way to get exp if you can get good at it. Gaming EXP is used to increase chances of counterattacking but doesn't give any extra health to the player. I also removed the exploit of being able to screwdriver arcade cabinets because people would do that if they thought they were on the verge of losing to bypass the effects of loss. I instead replaced it with a new interaction that the Curator's display case key can be used to reset arcade cabinets (there's several keys on the chain so it made sense to me), which I added solely because I thought Curators would be the type of person to have run an actual arcade. This is some gameplay https://github.com/tgstation/tgstation/assets/53777086/499083f5-75cc-43b5-b457-017a012beede As a misc sidenote, I also split up the arcade file just like how Orion Trail was before, just for neat code organization. The Inn keeper is straight up just a photo of my localhost dude, he's not a player reference or anything it's not my actual character. I also have no idea how well balanced this is cause I suck at it lol. Battle Arcade is one of 3 last machines in my hackmd here to turn into TGUI https://hackmd.io/XLt5MoRvRxuhFbwtk4VAUA?view I've always thought the current version of battle arcade is quite lame and lacks any progression, like Orion Trail I thought that since I was moving this to TGUI, it would also be a perfect opportunity to revamp it and try to improve on where it failed before, especially since the alternative (NTOS Arcade) is also lame as hell and is even lamer than HTML battle arcade (spam mana, then spam health, then just spam attack, rinse and repeat). This will hopefully be more entertaining and give players sense that they are getting through a series of tasks rather than doing one same one again and again. 🆑 JohnFulpWillard, Zeek the Rat add: Battle Arcade has been completely overhauled in a new progression system, this time using TGUI. add: The Curator's keys can now reset arcade cabinets. balance: You now need to be literate to play arcade games, except for Mediborg's Amputation Adventure. fix: You can no longer screwdriver emagged arcade consoles. Accept your fate. fix: Silicons can no longer play Mediborg's Amputation Adventure. /🆑 --------- Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com> * Change setting item weight class to a setter to patch some weight class related shenanigans (#82494) ## About The Pull Request Fixes #81052 Fixes #58008 Setting weight class of items is now done via `update_weight_class`. I updated as many occurrences of manually setting `w_class` as I could find but I may have missed some. Let me know if you know of any I missed. This is done to allow datums to react to an item having its weight class changed. Humans and atom storage are two such datums which now react to having an item in its contents change weight class, to allow it to expel items that grow to a weight class beyond what is normally allowed. ## Changelog 🆑 Melbert fix: You can't fit items which are normally too large for a storage by fitting it in the storage when it is small, then growing it to a larger size. /🆑 * Material datum color update, plus touching up some material items (knight armor, tiles) (#82500) ## About The Pull Request Tries to bring the material datum colors in closer approximation to the stacks they're attached too. I literally used the colors on the stacks. some might need to be lighter or darker, but for the most part they'll look...closer to their actual material hues.  I've also tweaked the sprites of both the tile object and the actual material tile turf to give it the right shading.  In addition to the tiles, I've also updated the knight armor and helmet to look closer to the much higher quality plate armor already in the game. ## Why It's Good For The Game It bothered me that the material datum coloring was inconsistent with the actual colors used for the material stacks. When they were updated, and even before they were updated, material datum stuff just never looked _right_. I wanted to change that so that it looks just right. I did not like the old material knight armor whatsoever. It was a dithered mess, and seemed to already use parts of the standard plate armor but with all the actual shading removed or replaced with the wrong colors. This fixes that so that the armor is actually readable for what it is. ## Changelog 🆑 image: Updates the colors of various material datum to bring them closer in-line with their actual material stacks image: Improves the sprites for the material knight armor and helmet. /🆑 * LateInitialize is not allowed to call parent anymore (#82540) ## About The Pull Request I've seen a few cases in the past where LateInitialize is done cause of the init return value being set to do so for no real reason, I thought I should try to avoid that by ensuring LateInitialize isn't ever called without overriding. This fixes a ton of machine's LateInitialize not calling parent (mechpad, door buttons, message monitor, a lot of tram machines, abductor console, holodeck computer & disposal bin), avoiding having to set itself up to be connected to power. If they were intended to not connect to power, they should be using ``NO_POWER_USE`` instead. Also removes a ton of returns to LateInit when it's already getting it from parent regardless (many cases of that in machine code). ## Why It's Good For The Game I think this is better for coding standard reasons as well as just making sure we're not calling this proc on things that does absolutely nothing with them. A machine not using power can be seen evidently not using power with ``NO_POWER_USE``, not so much if it's LateInitialize not calling parent. ## Changelog 🆑 fix: Mech pads, door buttons, message monitors, tram machines, abductor consoles & holodeck computers now use power. /🆑 * Fix table top deconstruction (#82508) ## About The Pull Request Edited: updated changelog, read comments for changes in implementation details So previously, tables would let you use a wrench to fully deconstruct them, or a screwdriver to take off only their top. This, however, broke in two different ways in #82280, when their deconstruction logic got changed. First off, deconstructed tables would only drop the materials for their top and not their frame. For this, the primary culprit seems to be on line 307: https://github.com/tgstation/tgstation/blob/c34d56a45b0461f5e0fad3cc75e81580c3357119/code/game/objects/structures/tables_racks.dm#L300-L307 Where `new framestack(target_turf, framestackamount)` accidentally got an extra indent, and ended up in the less common half of the if-else chain. Just moving this outside of the if-else chain again fixes it. Secondly, tables had their own special deconstruction logic, which got 'standardized'. Issue. This was special to accommodate for having two different deconstruction logics: full or top only. With `deconstruct(...)` no longer being overridable, I feel it's awkward to attempt to proxy that information to the new `atom_deconstruct(...)` So we introduce a new method, `deconstruct_top`, for the screwdriver to use, which handles deconstructing only the top. ```dm /obj/structure/table/proc/deconstruct_top() var/obj/table_frame = new frame(loc) if(obj_flags & NO_DECONSTRUCTION) table_frame.obj_flags |= NO_DECONSTRUCTION else // Mimic deconstruction logic, only drop our materials without NO_DECONSTRUCTION var/turf/target_turf = get_turf(src) drop_top_mats(target_turf) qdel(src) ``` Mimicking the `NO_DECONSTRUCTION` logic of normal deconstruction, and copying over the flag onto its frames if need be. This fixes screwdriver deconstruction. ## Why It's Good For The Game Fixes #82503. We can now deconstruct the table top separately again, AND get the right materials back too. ## Changelog 🆑 00-Steven, SyncIt21 fix: Wrench table deconstruction gives the right materials again. fix: Screwdriver table deconstruction only deconstructs the top again. /🆑 * [NO GBP] Reagent grinders display reagents on examination (#82535) ## About The Pull Request - Fixes #82531 Somehow omitted this during the general maintenance thing ## Changelog 🆑 fix: Reagent grinders display reagents of its beaker on examination /🆑 * Monkeys now use height offset (and monkey tail works) (#81598) This PR adds the ability for monkeys to wear any jumpsuit in the game, and adds support for them to wear things like coats, gloves, and shoes (though this cannot be obtained in-game and is solely achieved through admins, which I also improved a bit upon by adding a defined bitfield for no equip flags). This reverts a lot of changes from https://github.com/tgstation/tgstation/pull/73325 - We no longer check height from limbs and such to bring sprites down, instead monkeys now work more similarly to humans, so the entire PR was made irrelevant, and I didn't really want to leave around dead code for the sake of having a human with longer legs. I've now also added support for Dwarfism, which makes monkeys look even smaller. Very minor change but at least now the mutation doesn't feel like it does literally nothing to monkeys (since they can already walk over tables). Here's a few examples of how it can appear in game (purely for demonstration, as it is currently intentionally made impossible to obtain in-game, though if someone wants to change that post-this PR now that support is added, feel free): Tails have been broken for a while now, the only reason you see them in-game is because they are baked into the monkey sprites. This fixes that, which means humans can now get monkey tails implanted into them (hell yeah) and monkeys can have their tails removed (also hell yeah) * Gets [weird] with (spies) by adding protect and deuteragonist-flavored objectives. (#82447) ## About The Pull Request What are their goals? Why are they doing this? gets weird with Spy objectives - namely by adding a lot more ways spies might be asked to affect various targets around the station. the first of these is by several flavors of Protecting targets (these do NOT print a success at roundend in keeping with Spy design:) - Protect (get a humanoid target off alive) - Protect Nonhuman (get an entity off alive) - Jailbreak (make sure a humanoid target escapes free) - Detain (make sure a humanoid target gets taken out arrested) the second of this is by a new escape condition: - Exile (get off-station or off the Z-level by the end of the shift - sometimes it's not just pods, you need to fuck off to space to win.) the third is through a massive increase in the number of possible: - objective templates - departments to target (Command + Service added) - specific locations to target - general classes of objects to target (medicines, floor tiles, critical infrastructure, etc.) - efforts to target (such as meals, mechs, public supplies) - ways to leave (you can be asked to abscond from the scene of your crimes?) ## Why It's Good For The Game More goofy and weird prompts to do more interesting things with Spies. One thing I think we're sorely missing in our lineup is antagonists that can act a bit more as deuteragonists - very possibly helping the crew under certain conditions and frustrating the Hell out of them in others. Since there's no way to check their objectives, and they get their gear/progression through stealing shit, they're still very much an antagonist and exist under the suspicion of doing bad... but, just going by their objectives, introducing more varied (and in some cases even benign) goals for them creates suggestions pointing to a lot more varied and interesting stories if people choose to run with it. * Adds anosmia quirk (#82206) ## About The Pull Request Adds anosmia quirk. Anosmia, also known as smell blindness, is the loss of the ability to detect one or more smells. I tried to find all smells action and (most likely) update all of them, unfortunately I can't change descriptions for this quirk. ## Why It's Good For The Game Some characters will be able to not feel smells That affect: * Gases feelings and alerts (CO2, Plasma, miasm) - you don't feel them * Bakery and cooking * Changeling ability to feel other changelings by smell * Some unimportant spans * Explosions Part I - Directional Explosions (#82429) ## About The Pull Request Adds the ability for explosions to be directional. This is achieved by adding an angle check to `prepare_explosion_turfs()` to drop any turfs outside the cone of the explosion. If the arc covers a full 360 degrees, as is the default, it will accept all the turfs without performing the angle check. Uses this functionality to rework both rocket launcher backblast and X4 explosions. Rocket launcher backblast has been changed from a shotgun of indendiary bullets to a directional explosion of similar length. X4 now uses a directional explosion to "ensure user safety". Apparently the old method of moving the explosion one tile away didn't even work, as it blew up `target` before trying to check its density for the directional behaviour. https://youtu.be/Mzdt7d7Le2Y ## Why It's Good For The Game Directional explosions - Useful functionality for a range of potential use cases, which can be implemented with minimal extra processing cost (Worst case scenario being very large directional explosions) Backblast - Looks way cooler than a bunch of projectiles, and should be significantly more functional in high-lag situations where projectile code tends to get fucky X4 - More predictable for players wanting to use it as a breaching charge, you can actually stand near the charge and not have to worry about being hoist upon your own petard. ## Changelog 🆑 add: Added support for directional explosions. add: Rocket launcher backblast is now 271% more explosive, check your six for friendlies! add: X4 charges now explode in a cone away from the user when placed on a sufficiently solid object. fix: X4 charges will now behave correctly when placed on dense atoms (note: don't try to read a variable from an atom you just blew up) /🆑 * Add balloon alerts to plunging (#82559) ## About The Pull Request Makes all plunging actions (pretty much anything using `plunger_act`) have a visible balloon alert. ## Why It's Good For The Game Makes sense that others would easily notice you plunging the shit out of something. Also, more people might finally learn that you can plunge the vent clogs instead of welding them. ## Changelog 🆑 qol: Added balloon alerts whenever you start plunging something (i.e ) /🆑 * Fixes spurious runtime on Icemoon caused by turf calling unimplemented LateInitialize() (#82572) ## About The Pull Request As of https://github.com/tgstation/tgstation/pull/82540 this runtime was happening,  `/turf/open/openspace/icemoon/` can be changed to `/turf/open/misc/asteroid/snow/icemoon/do_not_chasm` before `Initialize()` returns, which resulted in it `INITIALIZE_HINT_LATELOAD` getting returned on a turf that does not have an implementation of that proc. This should fix that. ## Why It's Good For The Game Fixes CI error * Blueprints tgui (#82565) Blueprints now use a TGUI panel instead of the old HTML one. Also did general code improvement and maintaining to blueprints in general and also destroyed the ``areaeditor`` level, repathing it to just 'blueprints'. Also adds a sound when you look at structural data cause why not Video demonstration: https://github.com/tgstation/tgstation/assets/53777086/861773fd-3d57-472d-bc94-d67b0d4f1dbd The 4 blueprint types:  Another HTML menu dead underground. This is more responsive and doesn't require constant updating to see which area you're in, feels less OOC (instead of saying "the blueprints say", just say it, you ARE the blueprints). Like, come on  Look at all this wasted space  🆑 refactor: Blueprints now use TGUI. qol: Blueprints can now be used while lying down. /🆑 * General maintenance for chem master (#82002) **1. Qol** - Adds screen tips & examines for screwdriver, wrench, crowbar & beaker insertion, removal & replacing actions - Analyzing reagents is now a client side feature & not a back end mode, meaning one person can see details of a reagent while the other can print stuff and do other operations so it's a non blocking operation. This also means 2 players can see information of 2 different reagents in their own screens, With that the overlay for analysis mode has been removed - You cannot do any tool acts on machines while printing. Balloon alerts will be displayed warning you of that. - The preferred container for the master reagent in the beaker is now showed in both condiment & chem master. It can be enabled/disabled via a CheckBox **2. Code Improvements** - Removed defines like `TARGET_BEAKER` , `TARGET_BEAKER` etc. ther functionality is implemented as params in the `transfer_reagent()` proc directly - Removed all variables relating to analyzing reagents like `reagent_analysis_mode`, `has_container_suggestion` etc. all memory savings - `printable_containers` now stores static values that can be shared across many chem masters - Updates only overlays and not the whole icon during operations for efficiency **3. Fixes** - You can hit the chem master with the screwdriver, wrench, crowbar & beaker when in combat mode - You cannot insert hologram items into the chem master - Deconstructing a condiment master will give you the circuit board already pre-programmed with that option selected so you don't need to use a screwdriver to re program it - `printing_amount` is now the maximum number of containers that can be printed at a time. Presently this number with upgraded parts would print out empty containers especially for patches. This is because `volume_per_item` does not take into consideration this var. Also this var would not give control to the player on exactly how many containers to print as whatever amount the player entered would be multiplied with this value producing a lot of waste & worse empty containers. Now this var determines exactly how many containers you can print and is imposed on the client side UI as well **4. Refactors (UI performance)** - Beaker data is compressed into a single entity & sent to the UI. This is set to null if no beaker is loaded thus saving data sent - Reuses Beaker props from chem synthesizer to reduce code - reagent REF replaced with direct type converted to text and later converted with `text2path()` cause its much faster 🆑 qol: Adds screen tips & examines for screwdriver, wrench, crowbar & beaker insertion, removal & replacing actions qol: Analyzing reagents no longer blocks other players from doing other operations. Multiple players can analyze different reagents on the same machine qol: You cannot do any tool acts on the machine while printing to prevent any side effects. qol: The preferred container for the master reagent in the beaker is now showed in both condiment & chem master. The feature can be enabled/disabled via a check box code: removed defines for reagent transfer, vars for reagent analyzis to save memory. Autodoc for other vars & procs fix: You can hit the chem master with tools like screwdriver, crowbar, wrench & beaker in combat mode fix: You cannot insert hologram items into the chem master fix: Deconstructing a condiment master will give you the circuit board already pre-programmed with that option fix: You now print the exact amount of containers requested even with upgraded parts without creating empty containers. Max printable containers is 13 with tier 4 parts able to print 50 containers. refactor: Optimized client side UI code & chem master as a whole. /🆑 --------- Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com> * Wraps `lowertext()` to ensure proper stringification. (#82442) Fixes #82440 This PR just creates a new macro, `LOWER_TEXT()` (yes the irony is not lost on me) to wrap around all calls of `lowertext()` and ensure that whatever we input into that proc will be stringified using the `"[]"` (or `tostring()` for the nerds) operator. very simple. I also added a linter to enforce this (and prevent all forms of regression) because I think that machines should do the menial work and we shouldn't expect maintainers to remember this, let me know if you disagree. if there is a time when it should be opted out for some reason, the linter does respect it if you wrap your input with the `UNLINT()` function. * Clowns can now make balloon... toys. And also mallets and hats. (#82288) <!-- Write **BELOW** The Headers and **ABOVE** The comments else it may not be viewable. --> <!-- You can view Contributing.MD for a detailed description of the pull request process. --> Clowns will now start with a box of 24 random long balloons and a skillchip in their noggin allowing them to create balloon animals by combining two of them of different colour together. Owners of the skillchip also gain access to crafting recepies of balloon mallets, vests, helmets and tophats, all created from long balloons. A crate of long balloons, with a box of balloons inside, can be bought at cargo, in case the clown runs out. I might edit this once I wake up, its 3 in the morning right now. Oh also, resprited how balloons look in inventory.  Balloon animals funny. Silly features are my favourite kind of features, and this one's open-ended too. Someone on the coder chat recommended someone would do it that one time, here it goes. <!-- If your PR modifies aspects of the game that can be concretely observed by players or admins you should add a changelog. If your change does NOT meet this description, remove this section. Be sure to properly mark your PRs to prevent unnecessary GBP loss. You can read up on GBP and it's effects on PRs in the tgstation guides for contributors. Please note that maintainers freely reserve the right to remove and add tags should they deem it appropriate. You can attempt to finagle the system all you want, but it's best to shoot for clear communication right off the bat. --> 🆑 add: Added long balloon box to the clown's starting inventory, and a skill-chip of long lost honk-motherian knowledge to their brain. add: Added long balloons. Consequently, added balloon animals to make from such balloons. Also, balloon top hat, vest, helmet, and a mallet. Don't ask about the mallet. add: A long balloons box harvested fresh from the farms on the clown planet will be able to be shipped in a crate to the cargo department near you! add: As per requests; water balloons can now be printed at service lathe, and entertainment modsuit can now blow long balloons! image: Balloons will now have an unique sprite when in the inventory, compared when to on the ground. /🆑 <!-- Both 🆑's are required for the changelog to work! You can put your name to the right of the first 🆑 if you want to overwrite your GitHub username as author ingame. --> <!-- You can use multiple of the same prefix (they're only used for the icon ingame) and delete the unneeded ones. Despite some of the tags, changelogs should generally represent how a player might be affected by the changes rather than a summary of the PR's contents. --> --------- Co-authored-by: _0Steven <42909981+00-Steven@users.noreply.github.com> Co-authored-by: san7890 <the@san7890.com> Co-authored-by: Jacquerel <hnevard@gmail.com> * Quick spellcheck 'steall' (#82560) ## About The Pull Request https://github.com/tgstation/tgstation/pull/82447 quick followup to this, caught it while glancing through the code. * Fix * merge conflicts * Revert "Monkeys now use height offset (and monkey tail works) (#81598)" This reverts commit 5cfdc5972d16c6b509220e8874a927696249d36a. * fix * Fixed lateinitialize * This should cut it * Oh right * There? * Damn, here? * There * [NO GBP] Fixes spurious runtime caused by icemoon (again) (#82582) ## About The Pull Request https://github.com/tgstation/tgstation/pull/82572 I tried to fix this but there was an unaccounted race condition which just caused a separate runtime...  Since the type is being changed mid-execution `replacement_turf` will become out of scope. My bad--this should fix it now for good. --------- Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com> Co-authored-by: John Willard <53777086+JohnFulpWillard@users.noreply.github.com> Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com> Co-authored-by: necromanceranne <40847847+necromanceranne@users.noreply.github.com> Co-authored-by: _0Steven <42909981+00-Steven@users.noreply.github.com> Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> Co-authored-by: Higgin <cdonny11@yahoo.com> Co-authored-by: NeonNik2245 <106491639+NeonNik2245@users.noreply.github.com> Co-authored-by: Thunder12345 <Thunder12345@users.noreply.github.com> Co-authored-by: Lucy <lucy@absolucy.moe> Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com> Co-authored-by: san7890 <the@san7890.com> Co-authored-by: YesterdaysPromise <122572637+YesterdaysPromise@users.noreply.github.com> Co-authored-by: Jacquerel <hnevard@gmail.com> Co-authored-by: Useroth <37159550+Useroth@users.noreply.github.com>
786 lines
30 KiB
Plaintext
786 lines
30 KiB
Plaintext
#define EXTERNALREPLYCOUNT 2
|
|
#define EXTERNAL_PM_USER "IRCKEY"
|
|
|
|
// HEY FUCKO, IMPORTANT NOTE!
|
|
// This file, and pretty much everything that directly handles ahelps, is VERY important
|
|
// An admin pm dropping by coding error is disastorus, because it gives no feedback to admins, so they think they're being ignored
|
|
// It is imparitive that this does not happen. Therefore, runtimes are not allowed in this file
|
|
// Additionally, any runtimes here would cause admin tickets to leak into the runtime logs
|
|
// This is less of a big deal, but still bad
|
|
//
|
|
// In service of this goal of NO RUNTIMES then, we make ABSOLUTELY sure to never trust the nullness of a value
|
|
// That's why variables are so separated from logic here. It's not a good pattern typically, but it helps make assumptions clear here
|
|
// We also make SURE to fail loud, IE: if something stops the message from reaching the recipient, the sender HAS to know
|
|
// If you "refactor" this to make it "cleaner" I will send you to hell
|
|
|
|
ADMIN_VERB_ONLY_CONTEXT_MENU(cmd_admin_pm_context, R_NONE, "Admin PM Mob", mob/target in world)
|
|
if(!ismob(target))
|
|
to_chat(
|
|
src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM-Context: Target mob is not a mob, somehow."),
|
|
confidential = TRUE,
|
|
)
|
|
return
|
|
user.cmd_admin_pm(target.client, null)
|
|
BLACKBOX_LOG_ADMIN_VERB("Admin PM Mob")
|
|
|
|
ADMIN_VERB(cmd_admin_pm_panel, R_NONE, "Admin PM", "Show a list of clients to PM", ADMIN_CATEGORY_MAIN)
|
|
var/list/targets = list()
|
|
for(var/client/client in GLOB.clients)
|
|
var/nametag = ""
|
|
var/mob/lad = client.mob
|
|
var/mob_name = lad?.name
|
|
var/real_mob_name = lad?.real_name
|
|
if(!lad)
|
|
nametag = "(No Mob)"
|
|
else if(isnewplayer(lad))
|
|
nametag = "(New Player)"
|
|
else if(isobserver(lad))
|
|
nametag = "[mob_name](Ghost)"
|
|
else
|
|
nametag = "[real_mob_name](as [mob_name])"
|
|
targets["[nametag] - [client]"] = client
|
|
|
|
var/target = input(src,"To whom shall we send a message?", "Admin PM", null) as null|anything in sort_list(targets)
|
|
if (isnull(target))
|
|
return
|
|
user.cmd_admin_pm(targets[target], null)
|
|
BLACKBOX_LOG_ADMIN_VERB("Admin PM")
|
|
|
|
/// Replys to some existing ahelp, reply to whom, which can be a client or ckey
|
|
/client/proc/cmd_ahelp_reply(whom)
|
|
if(IsAdminAdvancedProcCall())
|
|
return FALSE
|
|
|
|
if(prefs.muted & MUTE_ADMINHELP)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM-Reply: You are unable to use admin PM-s (muted)."),
|
|
confidential = TRUE)
|
|
return
|
|
|
|
// We use the ckey here rather then keeping the client to ensure resistance to client logouts mid execution
|
|
if(istype(whom, /client))
|
|
var/client/boi = whom
|
|
whom = boi.ckey
|
|
|
|
var/ambiguious_recipient = disambiguate_client(whom)
|
|
if(!istype(ambiguious_recipient, /client))
|
|
if(holder)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM-Reply: Client not found."),
|
|
confidential = TRUE)
|
|
return
|
|
|
|
// Existing client case
|
|
var/client/recipient = ambiguious_recipient
|
|
|
|
// The ticket our recipient is using
|
|
var/datum/admin_help/recipient_ticket = recipient?.current_ticket
|
|
// Any past interactions with the recipient ticket
|
|
var/datum/admin_help/recipient_interactions = recipient_ticket?.ticket_interactions
|
|
// Any opening interactions with the recipient ticket, IE: interactions started before the ticket first receives a response
|
|
var/datum/admin_help/opening_interactions = recipient_ticket?.opening_responders
|
|
// Our recipient's admin holder, if one exists
|
|
var/datum/admins/recipient_holder = recipient?.holder
|
|
// The ckey of our recipient
|
|
var/recipient_ckey = recipient?.ckey
|
|
// Our recipient's fake key, if they are faking their ckey
|
|
var/recipient_fake_key = recipient_holder?.fakekey
|
|
// Our ckey, with our mob's name if one exists, formatted with a reply link
|
|
var/our_linked_name = key_name_admin(src)
|
|
// The recipient's ckey, formatted with a reply link
|
|
var/recipient_linked_ckey = key_name_admin(recipient, FALSE)
|
|
// The recipient's ckey, formatted slightly with html
|
|
var/formatted_recipient_ckey = key_name(recipient, FALSE, FALSE)
|
|
|
|
var/message_prompt = "Message:"
|
|
if(recipient_ticket)
|
|
message_admins("[our_linked_name] has started replying to [recipient_linked_ckey]'s admin help.")
|
|
// If none's interacted with the ticket yet
|
|
if(length(recipient_interactions) == 1)
|
|
if(length(opening_interactions)) // Inform the admin that they aren't the first
|
|
var/printable_interators = english_list(opening_interactions)
|
|
SEND_SOUND(src, sound('sound/machines/buzz-sigh.ogg', volume=30))
|
|
message_prompt += "\n\n**This ticket is already being responded to by: [printable_interators]**"
|
|
// add the admin who is currently responding to the list of people responding
|
|
LAZYADD(recipient_ticket.opening_responders, src)
|
|
|
|
var/request = "Private message to"
|
|
if(recipient_fake_key)
|
|
request = "[request] an Administrator."
|
|
else
|
|
request = "[request] [formatted_recipient_ckey]."
|
|
|
|
var/message = input(src, message_prompt, request) as message|null
|
|
|
|
if(recipient_ticket)
|
|
LAZYREMOVE(recipient_ticket.opening_responders, src)
|
|
|
|
if (!message)
|
|
message_admins("[our_linked_name] has cancelled their reply to [recipient_linked_ckey]'s admin help.")
|
|
return
|
|
|
|
if(!recipient) //We lost the client during input, disconnected or relogged.
|
|
if(GLOB.directory[recipient_ckey]) // Client has reconnected, lets try to recover
|
|
whom = GLOB.directory[recipient_ckey]
|
|
else
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM-Reply: Client not found."),
|
|
confidential = TRUE)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = "[span_danger("<b>Message not sent:</b>")]<br>[message]",
|
|
confidential = TRUE)
|
|
if(recipient_ticket)
|
|
recipient_ticket.AddInteraction("<b>No client found, message not sent:</b><br>[message]")
|
|
return
|
|
cmd_admin_pm(whom, message)
|
|
|
|
//takes input from cmd_admin_pm_context, cmd_admin_pm_panel or /client/Topic and sends them a PM.
|
|
//Fetching a message if needed.
|
|
//whom here is a client, a ckey, or [EXTERNAL_PM_USER] if this is from tgs. message is the default message to send
|
|
/client/proc/cmd_admin_pm(whom, message)
|
|
if(prefs.muted & MUTE_ADMINHELP)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM: You are unable to use admin PM-s (muted)."),
|
|
confidential = TRUE)
|
|
return
|
|
|
|
if(!holder && !current_ticket) //no ticket? https://www.youtube.com/watch?v=iHSPf6x1Fdo
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("You can no longer reply to this ticket, please open another one by using the Adminhelp verb if need be."),
|
|
confidential = TRUE)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_notice("Message: [message]"),
|
|
confidential = TRUE)
|
|
return
|
|
|
|
// We use the ckey here rather then keeping the client to ensure resistance to client logouts mid execution
|
|
if(istype(whom, /client))
|
|
var/client/boi = whom
|
|
whom = boi.ckey
|
|
|
|
var/message_to_send = request_adminpm_message(disambiguate_client(whom), message)
|
|
if(!message_to_send)
|
|
return
|
|
|
|
if(!sends_adminpm_message(disambiguate_client(whom), message_to_send))
|
|
return
|
|
|
|
notify_adminpm_message(disambiguate_client(whom), message_to_send)
|
|
|
|
|
|
/// Requests an admin pm message to send
|
|
/// message_target here can be either [EXTERNAL_PM_USER], indicating that this message is intended for some external chat channel
|
|
/// or a /client, which we will then store info about to ensure logout -> logins are protected as expected
|
|
/// Accepts an optional existing message, which will be used in place of asking the recipient assuming all other conditions are met
|
|
/// Returns the message to send or null if no message is found
|
|
/// Sleeps
|
|
/client/proc/request_adminpm_message(ambiguious_recipient, existing_message = null)
|
|
if(IsAdminAdvancedProcCall())
|
|
return null
|
|
|
|
if(ambiguious_recipient == EXTERNAL_PM_USER)
|
|
if(!externalreplyamount) //to prevent people from spamming irc/discord
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM-Message: External reply cap hit."),
|
|
confidential = TRUE)
|
|
return null
|
|
var/msg = ""
|
|
if(existing_message)
|
|
msg = existing_message
|
|
else
|
|
msg = input(src,"Message:", "Private message to Administrator") as message|null
|
|
|
|
if(!msg)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM-Message: No message input."),
|
|
confidential = TRUE)
|
|
return null
|
|
|
|
if(holder)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM-Message: Use the admin IRC/Discord channel, nerd."),
|
|
confidential = TRUE)
|
|
return null
|
|
return msg
|
|
|
|
if(!istype(ambiguious_recipient, /client))
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM-Message: Client not found."),
|
|
confidential = TRUE)
|
|
return null
|
|
|
|
var/client/recipient = ambiguious_recipient
|
|
// Stored in case client is deleted between this and after the message is input
|
|
var/recipient_ckey = recipient?.ckey
|
|
// Stored in case client is deleted between this and after the message is input
|
|
var/datum/admin_help/recipient_ticket = recipient?.current_ticket
|
|
// Our current active ticket
|
|
var/datum/admin_help/our_ticket = current_ticket
|
|
// If our recipient is an admin, this is their admins datum
|
|
var/datum/admins/recipient_holder = recipient?.holder
|
|
// If our recipient has a fake name, this is it
|
|
var/recipient_fake_key = recipient_holder?.fakekey
|
|
// Just the recipient's ckey, formatted for htmlifying stuff
|
|
var/recipient_print_key = key_name(recipient, FALSE, FALSE)
|
|
|
|
// The message we intend on returning
|
|
var/msg = ""
|
|
if(existing_message)
|
|
msg = existing_message
|
|
else
|
|
var/request = "Private message to"
|
|
if(recipient_fake_key)
|
|
request = "[request] an Administrator."
|
|
else
|
|
request = "[request] [recipient_print_key]."
|
|
//get message text, limit it's length.and clean/escape html
|
|
msg = input(src,"Message:", request) as message|null
|
|
msg = trim(msg)
|
|
|
|
if(!msg)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM-Message: No message input."),
|
|
confidential = TRUE)
|
|
return null
|
|
|
|
if(recipient)
|
|
return msg
|
|
// Client has disappeared due to logout
|
|
if(GLOB.directory[recipient_ckey]) // Client has reconnected, lets try to recover
|
|
recipient = GLOB.directory[recipient_ckey]
|
|
return msg
|
|
|
|
// We don't tell standard users if a ticket drops because admins have a way to actually see
|
|
// Past tickets, and well, admins are the ones who might ban you if you ignore them
|
|
if(holder)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM-Message: Client not found."),
|
|
confidential = TRUE)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = "[span_danger("<b>Message not sent:</b>")]<br>[msg]",
|
|
confidential = TRUE)
|
|
if(recipient_ticket)
|
|
recipient_ticket.AddInteraction("<b>No client found, message not sent:</b><br>[msg]")
|
|
return null
|
|
if(our_ticket)
|
|
our_ticket.MessageNoRecipient(msg)
|
|
return null
|
|
|
|
/// Sends a pm message via the tickets system
|
|
/// message_target here can be either [EXTERNAL_PM_USER], indicating that this message is intended for some external chat channel
|
|
/// or a /client, in which case we send in the standard form
|
|
/// send_message is the raw message to send, it will be filtered and treated to ensure we do not break any text handling
|
|
/// Returns FALSE if the send failed, TRUE otherwise
|
|
/client/proc/sends_adminpm_message(ambiguious_recipient, send_message)
|
|
if(IsAdminAdvancedProcCall())
|
|
return FALSE
|
|
|
|
send_message = adminpm_filter_text(ambiguious_recipient, send_message)
|
|
if(!send_message)
|
|
return null
|
|
|
|
if (handle_spam_prevention(send_message, MUTE_ADMINHELP))
|
|
// handle_spam_prevention does its own "hey buddy ya fucker up here's what happen"
|
|
return FALSE
|
|
|
|
var/raw_message = send_message
|
|
|
|
if(holder)
|
|
send_message = emoji_parse(send_message)
|
|
|
|
var/keyword_parsed_msg = keywords_lookup(send_message)
|
|
// Stores a bit of html with our ckey, name, and a linkified string to click and rely to us with
|
|
var/name_key_with_link = key_name(src, TRUE, TRUE)
|
|
|
|
if(ambiguious_recipient == EXTERNAL_PM_USER)
|
|
var/datum/admin_help/new_admin_help = admin_ticket_log(src,
|
|
"<font color='red'>Reply PM from-<b>[name_key_with_link]</b> to <i>External</i>: [keyword_parsed_msg]</font>",
|
|
player_message = "<font color='red'>Reply PM from-<b>[name_key_with_link]</b> to <i>External</i>: [send_message]</font>")
|
|
|
|
new_admin_help.reply_to_admins_notification(raw_message)
|
|
|
|
var/new_help_id = new_admin_help?.id
|
|
|
|
externalreplyamount--
|
|
|
|
var/category = "Reply: [ckey]"
|
|
if(new_admin_help)
|
|
category = "#[new_help_id] [category]"
|
|
|
|
send2adminchat(category, raw_message)
|
|
return TRUE
|
|
|
|
if(!istype(ambiguious_recipient, /client))
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM-Send: Client not found."),
|
|
confidential = TRUE)
|
|
return FALSE
|
|
|
|
var/client/recipient = ambiguious_recipient
|
|
var/datum/admins/recipient_holder = recipient.holder
|
|
var/datum/admins/our_holder = holder
|
|
|
|
// The sound preferences of the recipient, at least that's how we'll be using it here
|
|
var/sound_prefs = recipient?.prefs?.toggles
|
|
// Stores a bit of html that contains the ckey of the recipient, its mob's name if any exist, and a link to reply to them with
|
|
var/their_name_with_link = key_name(recipient, TRUE, TRUE)
|
|
// Stores a bit of html with our ckey highlighted as a reply link
|
|
var/link_to_us = key_name(src, TRUE, FALSE)
|
|
// Stores a bit of html with outhe ckey of the recipientr highlighted as a reply link
|
|
var/link_to_their = key_name(recipient, TRUE, FALSE)
|
|
// Our ckey
|
|
var/our_ckey = ckey
|
|
// Recipient ckey
|
|
var/recip_ckey = recipient?.ckey
|
|
// Our current ticket, can (supposedly) be null here
|
|
var/datum/admin_help/ticket = current_ticket
|
|
// The recipient's current ticket, could in theory? maybe? be null here
|
|
var/datum/admin_help/recipient_ticket = recipient?.current_ticket
|
|
// I use -1 as a default for both of these
|
|
// Our ticket ID
|
|
var/ticket_id = ticket?.id
|
|
// The recipient's ticket id
|
|
var/recipient_ticket_id = recipient_ticket?.id
|
|
|
|
// If we should do a full on boink, so with the text and extra flair and everything
|
|
// We want to always do this so long as WE are an admin, and we're messaging the "loser" of the converstation
|
|
var/full_boink = FALSE
|
|
// Only admins can perform boinks
|
|
if(our_holder)
|
|
full_boink = TRUE
|
|
// Tickets will only generate for the non admin/admin being boinked. This check is to ensure boinked admins don't send the same
|
|
// ADMINISTRAITOR PRIVATE MESSAGE text to their boinker every time they respond
|
|
if(recipient_holder && ticket)
|
|
full_boink = FALSE
|
|
|
|
// If we're gonna boink em, do it now
|
|
// It is worth noting this will always generate the target a ticket if they don't already have one (tickets will generate if a player ahelps automatically, outside this logic)
|
|
// So past this point, because of our block above here, we can be reasonably guarenteed that the user will have a ticket
|
|
if(full_boink)
|
|
// Do BIG RED TEXT
|
|
var/already_logged = FALSE
|
|
// Full boinks will always be done to players, so we are not guarenteed that they won't have a ticket
|
|
if(!recipient_ticket)
|
|
new /datum/admin_help(send_message, recipient, TRUE, src) // SKYRAT EDIT - Handling tickets - ORIGINAL: new /datum/admin_help(send_message, recipient, TRUE)
|
|
already_logged = TRUE
|
|
// This action mutates our existing cached ticket information, so we recache
|
|
ticket = current_ticket
|
|
recipient_ticket = recipient?.current_ticket
|
|
ticket_id = ticket?.id
|
|
recipient_ticket_id = recipient_ticket?.id
|
|
SSblackbox.LogAhelp(recipient_ticket_id, "Ticket Opened", send_message, recipient.ckey, src.ckey)
|
|
|
|
to_chat(recipient,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = "<font color='red' size='4'><b>-- Administrator private message --</b></font>",
|
|
confidential = TRUE)
|
|
|
|
recipient.receive_ahelp(
|
|
link_to_us,
|
|
span_linkify(send_message),
|
|
)
|
|
|
|
to_chat(recipient,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_adminsay("<i>Click on the administrator's name to reply.</i>"),
|
|
confidential = TRUE)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_notice("Admin PM to-<b>[their_name_with_link]</b>: [span_linkify(send_message)]"),
|
|
confidential = TRUE)
|
|
|
|
admin_ticket_log(recipient,
|
|
"<font color='purple'>PM From [name_key_with_link]: [keyword_parsed_msg]</font>",
|
|
log_in_blackbox = FALSE,
|
|
player_message = "<font color='purple'>PM From [link_to_us]: [send_message]</font>")
|
|
|
|
if(!already_logged) //Reply to an existing ticket
|
|
SSblackbox.LogAhelp(recipient_ticket_id, "Reply", send_message, recip_ckey, our_ckey)
|
|
|
|
//always play non-admin recipients the adminhelp sound
|
|
SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg'))
|
|
return TRUE
|
|
|
|
//SKYRAT EDIT ADDITION BEGIN - ADMIN
|
|
// Basically, if we realized that we shouldn't've been handling the ticket, let's bail. Otherwise, we just change who's handling it.
|
|
if(ticket && our_holder && !ticket.handle_issue())
|
|
return
|
|
// SKYRAT EDIT END
|
|
|
|
// Ok if we're here, either this message is for an admin, or someone somehow figured out how to send a new message as a player
|
|
// First case well, first
|
|
if(!our_holder && !recipient_holder) //neither are admins
|
|
if(!ticket)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM-Send: Non-admin to non-admin PM communication is forbidden."),
|
|
confidential = TRUE)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = "[span_danger("<b>Message not sent:</b>")]<br>[send_message]",
|
|
confidential = TRUE)
|
|
return FALSE
|
|
ticket.MessageNoRecipient(send_message)
|
|
return TRUE
|
|
|
|
// Ok by this point the recipient has to be an admin, and this is either an admin on admin event, or a player replying to an admin
|
|
|
|
// You're replying to a ticket that is closed. Bad move. You must have started replying before the close, and then got input()'d
|
|
// Lets be nice and pass this off to a new ticket, as we recomend above
|
|
if(!ticket)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM-Send: Attempted to send a reply to a closed ticket."),
|
|
confidential = TRUE)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_notice("Relaying message to a new admin help."),
|
|
confidential = TRUE)
|
|
GLOB.admin_help_ui_handler.perform_adminhelp(src, raw_message, FALSE)
|
|
return FALSE
|
|
|
|
// Let's play some music for the admin, only if they want it tho
|
|
if(sound_prefs & SOUND_ADMINHELP)
|
|
SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg'))
|
|
|
|
SEND_SIGNAL(ticket, COMSIG_ADMIN_HELP_REPLIED)
|
|
|
|
// Admin on admin violence first
|
|
if(our_holder)
|
|
recipient.receive_ahelp(
|
|
name_key_with_link,
|
|
span_linkify(keyword_parsed_msg),
|
|
"danger",
|
|
)
|
|
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_notice("Admin PM to-<b>[their_name_with_link]</b>: [span_linkify(keyword_parsed_msg)]"),
|
|
confidential = TRUE)
|
|
|
|
//omg this is dumb, just fill in both their logs
|
|
var/interaction_message = "<font color='purple'>PM from-<b>[name_key_with_link]</b> to-<b>[their_name_with_link]</b>: [keyword_parsed_msg]</font>"
|
|
var/player_interaction_message = "<font color='purple'>PM from-<b>[link_to_us]</b> to-<b>[link_to_their]</b>: [send_message]</font>"
|
|
admin_ticket_log(src,
|
|
interaction_message,
|
|
log_in_blackbox = FALSE,
|
|
player_message = player_interaction_message)
|
|
if(recipient != src) //reeee
|
|
admin_ticket_log(recipient,
|
|
interaction_message,
|
|
log_in_blackbox = FALSE,
|
|
player_message = player_interaction_message)
|
|
|
|
SSblackbox.LogAhelp(ticket_id, "Reply", send_message, recip_ckey, our_ckey)
|
|
return TRUE
|
|
|
|
// This is us (a player) trying to talk to the recipient (an admin)
|
|
var/replymsg = "Reply PM from-<b>[name_key_with_link]</b>: [span_linkify(keyword_parsed_msg)]"
|
|
var/player_replymsg = "Reply PM from-<b>[link_to_us]</b>: [span_linkify(send_message)]"
|
|
admin_ticket_log(src,
|
|
"<font color='red'>[replymsg]</font>",
|
|
log_in_blackbox = FALSE,
|
|
player_message = player_replymsg)
|
|
to_chat(recipient,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("[replymsg]"),
|
|
confidential = TRUE)
|
|
|
|
ticket.reply_to_admins_notification(send_message)
|
|
SSblackbox.LogAhelp(ticket_id, "Reply", send_message, recip_ckey, our_ckey)
|
|
|
|
return TRUE
|
|
|
|
/// Notifies all admins about the existance of an admin pm, then logs the pm
|
|
/// message_target here can be either [EXTERNAL_PM_USER], indicating that this message is intended for some external chat channel
|
|
/// or a /client, in which case we send in the standard form
|
|
/// log_message is the raw message to send, it will be filtered and treated to ensure we do not break any text handling
|
|
/client/proc/notify_adminpm_message(ambiguious_recipient, log_message)
|
|
if(IsAdminAdvancedProcCall())
|
|
return
|
|
|
|
// First we filter, because these procs can be called by anyone with debug, and I don't trust that check
|
|
// gotta make sure none's fucking about
|
|
log_message = adminpm_filter_text(ambiguious_recipient, log_message)
|
|
if(!log_message)
|
|
return
|
|
|
|
var/raw_message = log_message
|
|
|
|
if(holder)
|
|
log_message = emoji_parse(log_message)
|
|
|
|
var/keyword_parsed_msg = keywords_lookup(log_message)
|
|
// Shows our ckey and the name of any mob we might be possessing
|
|
var/our_name = key_name(src)
|
|
// Shows our ckey/name embedded inside a clickable link to reply to this message
|
|
var/our_linked_ckey = key_name(src, TRUE, FALSE)
|
|
// Our current active ticket
|
|
var/datum/admin_help/ticket = current_ticket
|
|
// Our current ticket id, if one exists
|
|
var/ticket_id = ticket?.id
|
|
|
|
if(ambiguious_recipient == EXTERNAL_PM_USER)
|
|
// Guard against the possibility of a null, since it'll runtime and spit out the contents of what should be a private ticket.
|
|
if(ticket)
|
|
log_admin_private("PM: Ticket #[ticket_id]: [our_name]->External: [sanitize_text(trim(raw_message))]")
|
|
else
|
|
log_admin_private("PM: [our_name]->External: [sanitize_text(trim(raw_message))]")
|
|
for(var/client/lad in GLOB.admins)
|
|
to_chat(lad,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_notice("<B>PM: [our_linked_ckey]->External:</B> [keyword_parsed_msg]"),
|
|
confidential = TRUE)
|
|
return
|
|
if(!istype(ambiguious_recipient, /client))
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM-Notify: Client not found."),
|
|
confidential = TRUE)
|
|
return
|
|
|
|
var/client/recipient = ambiguious_recipient
|
|
// The key of our recipient
|
|
var/recipient_key = recipient?.key
|
|
// Shows the recipient's ckey and the name of any mob it might be possessing
|
|
var/recipient_name = key_name(recipient)
|
|
// Shows the recipient's ckey/name embedded inside a clickable link to reply to this message
|
|
var/recipient_linked_ckey = key_name(recipient, TRUE, FALSE)
|
|
|
|
window_flash(recipient, ignorepref = TRUE)
|
|
if(ticket)
|
|
log_admin_private("PM: Ticket #[ticket_id]: [our_name]->[recipient_name]: [sanitize_text(trim(raw_message))]")
|
|
else
|
|
log_admin_private("PM: [our_name]->[recipient_name]: [sanitize_text(trim(raw_message))]")
|
|
//we don't use message_admins here because the sender/receiver might get it too
|
|
for(var/client/lad in GLOB.admins)
|
|
if(lad.key == key || lad.key == recipient_key) //check to make sure client/lad isn't the sender or recipient
|
|
continue
|
|
to_chat(lad,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_notice("<B>PM: [our_linked_ckey]->[recipient_linked_ckey]:</B> [keyword_parsed_msg]") ,
|
|
confidential = TRUE)
|
|
|
|
/// Accepts a message and an ambiguious recipient (some sort of client representative, or [EXTERNAL_PM_USER])
|
|
/// Returns the filtered message if it passes all checks, or null if the send fails
|
|
/client/proc/adminpm_filter_text(ambiguious_recipient, message)
|
|
if(prefs.muted & MUTE_ADMINHELP)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM-Filter: You are unable to use admin PM-s (muted)."),
|
|
confidential = TRUE)
|
|
return
|
|
|
|
//clean the message if it's not sent by a high-rank admin
|
|
if(!check_rights(R_SERVER|R_DEBUG, 0) || ambiguious_recipient == EXTERNAL_PM_USER)//no sending html to the poor bots
|
|
message = sanitize(copytext_char(message, 1, MAX_MESSAGE_LEN))
|
|
if(!message)
|
|
to_chat(src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_danger("Error: Admin-PM-Filter: Your message contained only HTML, it's been sanitized away and the message disregarded."),
|
|
confidential = TRUE)
|
|
return
|
|
return message
|
|
|
|
#define TGS_AHELP_USAGE "Usage: ticket <close|resolve|icissue|reject|reopen \[ticket #\]|list>"
|
|
/proc/TgsPm(target, message, sender)
|
|
var/requested_ckey = ckey(target)
|
|
var/ambiguious_target = disambiguate_client(requested_ckey)
|
|
|
|
var/client/recipient
|
|
// This might seem like hiding a failure condition, but we want to be able to send commands to the ticket without the client being logged in
|
|
if(istype(ambiguious_target, /client))
|
|
recipient = ambiguious_target
|
|
|
|
// The ticket we want to talk about here. Either the target's active ticket, or the last one it had
|
|
var/datum/admin_help/ticket
|
|
if(recipient)
|
|
ticket = recipient.current_ticket
|
|
else
|
|
GLOB.ahelp_tickets.CKey2ActiveTicket(requested_ckey)
|
|
// The ticket's id
|
|
var/ticket_id = ticket?.id
|
|
|
|
var/compliant_msg = trim(LOWER_TEXT(message))
|
|
var/tgs_tagged = "[sender](TGS/External)"
|
|
var/list/splits = splittext(compliant_msg, " ")
|
|
var/split_size = length(splits)
|
|
|
|
if(split_size && splits[1] == "ticket")
|
|
if(split_size < 2)
|
|
return TGS_AHELP_USAGE
|
|
switch(splits[2])
|
|
if("close")
|
|
if(ticket)
|
|
ticket.Close(tgs_tagged)
|
|
return "Ticket #[ticket_id] successfully closed"
|
|
if("resolve")
|
|
if(ticket)
|
|
ticket.Resolve(tgs_tagged)
|
|
return "Ticket #[ticket_id] successfully resolved"
|
|
if("icissue")
|
|
if(ticket)
|
|
ticket.ICIssue(tgs_tagged)
|
|
return "Ticket #[ticket_id] successfully marked as IC issue"
|
|
if("reject")
|
|
if(ticket)
|
|
ticket.Reject(tgs_tagged)
|
|
return "Ticket #[ticket_id] successfully rejected"
|
|
if("reopen")
|
|
if(ticket)
|
|
return "Error: [target] already has ticket #[ticket_id] open"
|
|
var/ticket_num
|
|
// If the passed in command actually has a ticket id arg
|
|
if(split_size >= 3)
|
|
ticket_num = text2num(splits[3])
|
|
|
|
if(isnull(ticket_num))
|
|
return "Error: No/Invalid ticket id specified. [TGS_AHELP_USAGE]"
|
|
|
|
// The active ticket we're trying to reopen, if one exists
|
|
var/datum/admin_help/active_ticket = GLOB.ahelp_tickets.TicketByID(ticket_num)
|
|
// The ckey of the player to be targeted BY the ticket
|
|
// Not the initiator all the time
|
|
var/boinked_ckey = active_ticket?.initiator_ckey
|
|
|
|
if(!active_ticket)
|
|
return "Error: Ticket #[ticket_num] not found"
|
|
if(boinked_ckey != target)
|
|
return "Error: Ticket #[ticket_num] belongs to [boinked_ckey]"
|
|
|
|
active_ticket.Reopen()
|
|
return "Ticket #[ticket_num] successfully reopened"
|
|
if("list")
|
|
var/list/tickets = GLOB.ahelp_tickets.TicketsByCKey(target)
|
|
var/tickets_length = length(tickets)
|
|
|
|
if(!tickets_length)
|
|
return "None"
|
|
var/list/printable_tickets = list()
|
|
for(var/datum/admin_help/iterated_ticket in tickets)
|
|
// The id of the iterated adminhelp
|
|
var/iterated_id = iterated_ticket?.id
|
|
var/text = ""
|
|
if(iterated_ticket == ticket)
|
|
text += "Active: "
|
|
text += "#[iterated_id]"
|
|
printable_tickets += text
|
|
return printable_tickets.Join(", ")
|
|
else
|
|
return TGS_AHELP_USAGE
|
|
return "Error: Ticket could not be found"
|
|
|
|
// Now that we've handled command processing, we can actually send messages to the client
|
|
if(!recipient)
|
|
return "Error: No client"
|
|
|
|
var/adminname
|
|
if(CONFIG_GET(flag/show_irc_name))
|
|
adminname = tgs_tagged
|
|
else
|
|
adminname = "Administrator"
|
|
|
|
var/stealthkey = GetTgsStealthKey()
|
|
|
|
message = sanitize(copytext_char(message, 1, MAX_MESSAGE_LEN))
|
|
message = emoji_parse(message)
|
|
|
|
if(!message)
|
|
return "Error: No message"
|
|
|
|
// The ckey of our recipient, with a reply link, and their mob if one exists
|
|
var/recipient_name_linked = key_name_admin(recipient)
|
|
// The ckey of our recipient, with their mob if one exists. No link
|
|
var/recipient_name = key_name_admin(recipient)
|
|
|
|
message_admins("External message from [sender] to [recipient_name_linked] : [message]")
|
|
log_admin_private("External PM: [sender] -> [recipient_name] : [message]")
|
|
|
|
to_chat(recipient,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = "<font color='red' size='4'><b>-- Administrator private message --</b></font>",
|
|
confidential = TRUE)
|
|
|
|
recipient.receive_ahelp(
|
|
"<a href='?priv_msg=[stealthkey]'>[adminname]</a>",
|
|
message,
|
|
)
|
|
|
|
to_chat(recipient,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = span_adminsay("<i>Click on the administrator's name to reply.</i>"),
|
|
confidential = TRUE)
|
|
|
|
admin_ticket_log(recipient, "<font color='purple'>PM From [tgs_tagged]: [message]</font>", log_in_blackbox = FALSE)
|
|
|
|
window_flash(recipient, ignorepref = TRUE)
|
|
// Nullcheck because we run a winset in window flash and I do not trust byond
|
|
if(recipient)
|
|
//always play non-admin recipients the adminhelp sound
|
|
SEND_SOUND(recipient, 'sound/effects/adminhelp.ogg')
|
|
|
|
recipient.externalreplyamount = EXTERNALREPLYCOUNT
|
|
return "Message Successful"
|
|
|
|
/// Gets TGS's stealth key, generates one if none is found
|
|
/proc/GetTgsStealthKey()
|
|
var/static/tgsStealthKey
|
|
if(tgsStealthKey)
|
|
return tgsStealthKey
|
|
|
|
tgsStealthKey = generateStealthCkey()
|
|
GLOB.stealthminID[EXTERNAL_PM_USER] = tgsStealthKey
|
|
return tgsStealthKey
|
|
|
|
/// Takes an argument which could be either a ckey, /client, or IRC marker, and returns a client if possible
|
|
/// Returns [EXTERNAL_PM_USER] if an IRC marker is detected
|
|
/// Otherwise returns null
|
|
/proc/disambiguate_client(whom)
|
|
if(istype(whom, /client))
|
|
return whom
|
|
|
|
if(!istext(whom) || !(length(whom) >= 1))
|
|
return null
|
|
|
|
var/searching_ckey = whom
|
|
if(whom[1] == "@")
|
|
searching_ckey = findTrueKey(whom)
|
|
|
|
if(searching_ckey == EXTERNAL_PM_USER)
|
|
return EXTERNAL_PM_USER
|
|
|
|
return GLOB.directory[searching_ckey]
|
|
|
|
/client/proc/receive_ahelp(reply_to, message, span_class = "adminsay")
|
|
to_chat(
|
|
src,
|
|
type = MESSAGE_TYPE_ADMINPM,
|
|
html = "<span class='[span_class]'>Admin PM from-<b>[reply_to]</b>: [message]</span>",
|
|
confidential = TRUE,
|
|
)
|
|
|
|
current_ticket?.player_replied = FALSE
|
|
|
|
SEND_SIGNAL(src, COMSIG_ADMIN_HELP_RECEIVED, message)
|
|
|
|
#undef EXTERNAL_PM_USER
|
|
#undef EXTERNALREPLYCOUNT
|
|
#undef TGS_AHELP_USAGE
|