Files
Bubberstation/code/modules/admin/sql_ban_system.dm
SpaceLoveSs13 7aa6664021 Mirror (#27453)
* 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.

![material datum stuff
2](https://github.com/tgstation/tgstation/assets/40847847/a3a5968f-cab3-43dd-8699-3dae879b21fb)

I've also tweaked the sprites of both the tile object and the actual
material tile turf to give it the right shading.


![image](https://github.com/tgstation/tgstation/assets/40847847/56489479-8774-492b-b8cc-aeabc6c0a67f)

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:

c34d56a45b/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,


![image](https://github.com/tgstation/tgstation/assets/13398309/4d838fda-c157-4e33-ae64-77eafca1806f)

`/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:

![image](https://github.com/tgstation/tgstation/assets/53777086/bfd68eb5-c430-4608-a3f7-d6ac86727882)

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

![image](https://github.com/tgstation/tgstation/assets/53777086/fcf1b8b6-37c3-4c94-969f-3b121c983dc0)

Look at all this wasted space

![image](https://github.com/tgstation/tgstation/assets/53777086/1d3e61cd-bc56-4280-a3bb-0c66604e75b3)

🆑
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.

![image](https://github.com/tgstation/tgstation/assets/122572637/2d7ce955-c787-48a5-bfda-4613d2eed837)

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...


![firefox_GxGibAO281](https://github.com/tgstation/tgstation/assets/13398309/f251aecc-ef17-4bae-a741-5aade40de423)

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>
2024-04-26 01:22:24 +02:00

1169 lines
51 KiB
Plaintext

#define MAX_ADMINBANS_PER_ADMIN 1
#define MAX_ADMINBANS_PER_HEADMIN 3
#define MAX_REASON_LENGTH 600
/**
* Checks client ban cache or, if it doesn't exist, queries the DB ban table to see if the player's
* ckey is banned from at least one of the provided roles.
*
* Returns TRUE if the player matches with one or more role bans.
* Returns FALSE if the player doesn't match with any role bans. Possible errors states also return FALSE.
*
* Args:
* * player_key - Either key or ckey of the player you want to check for role bans.
* * roles - Accepts either a single role string, or a list of role strings.
*/
/proc/is_banned_from(player_key, list/roles)
if(!player_key)
stack_trace("Called is_banned_from without specifying a ckey.")
return FALSE
// Convert to ckey. This allows is_banned_from to take either key or ckey interchangably,
// and is officially a feature of the proc.
var/player_ckey = ckey(player_key)
var/client/player_client = GLOB.directory[player_ckey]
// If there's a player client, we try to set up their ban cache (if it doesn't already exist) and test from that.
if(player_client)
var/list/ban_cache = retrieve_ban_cache(player_client)
// If this isn't a list, the client disconnected while building it.
if(!islist(ban_cache))
return FALSE
// If it is a list, check each role.
if(islist(roles))
for(var/role in roles)
if(role in ban_cache)
return TRUE //they're banned from at least one role, no need to keep checking
return FALSE
// Otherwise, it's just a single role string. Return if it's in the ban cache.
return (roles in ban_cache)
// If there's no player client, we'll ask the database.
var/values = list(
"player_ckey" = player_ckey,
"must_apply_to_admins" = !!(GLOB.admin_datums[player_ckey] || GLOB.deadmins[player_ckey]),
)
// SKYRAT EDIT ADDITION BEGIN - MULTISERVER
var/ssqlname = CONFIG_GET(string/serversqlname)
var/server_check
if(CONFIG_GET(flag/respect_global_bans))
server_check = "(server_name = '[ssqlname]' OR global_ban = '1')"
else
server_check = "server_name = '[ssqlname]'"
// SKYRAT EDIT ADDITION END - MULTISERVER
var/sql_roles
if(islist(roles))
var/list/sql_roles_list = list()
for (var/i in 1 to roles.len)
values["role[i]"] = roles[i]
sql_roles_list += ":role[i]"
sql_roles = sql_roles_list.Join(", ")
else
values["role"] = roles
sql_roles = ":role"
var/datum/db_query/query_check_ban = SSdbcore.NewQuery(/* SKYRAT EDIT CHANGE - MULTISERVER */{"
SELECT 1
FROM [format_table_name("ban")]
WHERE
ckey = :player_ckey AND
role IN ([sql_roles]) AND
unbanned_datetime IS NULL AND
(expiration_time IS NULL OR expiration_time > NOW())
AND [server_check]
AND (NOT :must_apply_to_admins OR applies_to_admins = 1)
"}, values)
// If there's an SQL error, return FALSE.
if(!query_check_ban.warn_execute())
qdel(query_check_ban)
return FALSE
// If there are any rows, we got a match and they're role banned from at least one role.
if(query_check_ban.NextRow())
qdel(query_check_ban)
return TRUE
// Otherwise, they're not banned from any roles in the DB.
qdel(query_check_ban)
return FALSE
//checks DB ban table if a ckey, ip and/or cid is banned from a specific role
//returns an associative nested list of each matching row's ban id, bantime, ban round id, expiration time, ban duration, applies to admins, reason, key, ip, cid and banning admin's key in that order
/proc/is_banned_from_with_details(player_ckey, player_ip, player_cid, role)
if(!player_ckey && !player_ip && !player_cid)
return
var/ssqlname = CONFIG_GET(string/serversqlname) // SKYRAT EDIT ADDITION BEGIN - MULTISERVER
var/server_check
if(CONFIG_GET(flag/respect_global_bans))
server_check = "(server_name = '[ssqlname]' OR global_ban = '1')"
else
server_check = "server_name = '[ssqlname]'" // SKYRAT EDIT ADDITION END - MULTISERVER
var/datum/db_query/query_check_ban = SSdbcore.NewQuery(/* SKYRAT EDIT CHANGE - MULTISERVER */{"
SELECT
id,
bantime,
round_id,
expiration_time,
TIMESTAMPDIFF(MINUTE, bantime, expiration_time),
applies_to_admins,
reason,
IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE [format_table_name("player")].ckey = [format_table_name("ban")].ckey), ckey),
INET_NTOA(ip),
computerid,
IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE [format_table_name("player")].ckey = [format_table_name("ban")].a_ckey), a_ckey)
FROM [format_table_name("ban")]
WHERE role = :role
AND (ckey = :ckey OR ip = INET_ATON(:ip) OR computerid = :computerid)
AND unbanned_datetime IS NULL
AND [server_check]
AND (expiration_time IS NULL OR expiration_time > NOW())
ORDER BY bantime DESC
"}, list("role" = role, "ckey" = player_ckey, "ip" = player_ip, "computerid" = player_cid))
if(!query_check_ban.warn_execute())
qdel(query_check_ban)
return
. = list()
while(query_check_ban.NextRow())
. += list(list("id" = query_check_ban.item[1], "bantime" = query_check_ban.item[2], "round_id" = query_check_ban.item[3], "expiration_time" = query_check_ban.item[4], "duration" = query_check_ban.item[5], "applies_to_admins" = query_check_ban.item[6], "reason" = query_check_ban.item[7], "key" = query_check_ban.item[8], "ip" = query_check_ban.item[9], "computerid" = query_check_ban.item[10], "admin_key" = query_check_ban.item[11]))
qdel(query_check_ban)
/// Gets the ban cache of the passed in client
/// If the cache has not been generated, we start off a query
/// If we still have a query going for this request, we just sleep until it's received back
/proc/retrieve_ban_cache(client/player_client)
if(QDELETED(player_client))
return
if(player_client.ban_cache)
return player_client.ban_cache
var/config_delay = CONFIG_GET(number/blocking_query_timeout) SECONDS
// If we haven't got a query going right now, or we've timed out on the old query
if(player_client.ban_cache_start + config_delay < REALTIMEOFDAY)
return build_ban_cache(player_client)
// Ok so we've got a request going, lets start a wait cycle
// If we wait longer then config/db_ban_timeout we'll send another request
// We use timeofday here because we're talking human time
// We do NOT cache the start time because it can update, and we want it to be able to
while(player_client && player_client?.ban_cache_start + config_delay >= REALTIMEOFDAY && !player_client?.ban_cache)
// Wait a decisecond or two would ya?
// If this causes lag on client join, increase this delay. it doesn't need to be too fast since this should
// Realllly only happen near Login, and we're unlikely to make any new requests in that time
stoplag(2)
// If we have a ban cache, use it. if we've timed out, go ahead and start another query would you?
// This will update any other sleep loops, so we should only run one at a time
return player_client?.ban_cache || build_ban_cache(player_client)
/proc/build_ban_cache(client/player_client)
if(!SSdbcore.Connect())
return
if(QDELETED(player_client))
return
var/current_time = REALTIMEOFDAY
player_client.ban_cache_start = current_time
var/ckey = player_client.ckey
var/list/ban_cache = list()
var/is_admin = FALSE
if(GLOB.admin_datums[ckey] || GLOB.deadmins[ckey])
is_admin = TRUE
// SKYRAT EDIT ADDITION BEGIN - MULTISERVER
var/ssqlname = CONFIG_GET(string/serversqlname)
var/server_check
if(CONFIG_GET(flag/respect_global_bans))
server_check = "(server_name = '[ssqlname]' OR global_ban = '1')"
else
server_check = "server_name = '[ssqlname]'"
// SKYRAT EDIT ADDITION END - MULTISERVER
var/datum/db_query/query_build_ban_cache = SSdbcore.NewQuery(
"SELECT role, applies_to_admins FROM [format_table_name("ban")] WHERE ckey = :ckey AND unbanned_datetime IS NULL AND (expiration_time IS NULL OR expiration_time > NOW()) AND [server_check]", //skyrat edit
list("ckey" = ckey)
)
var/query_successful = query_build_ban_cache.warn_execute()
// After we sleep, we check if this was the most recent cache build, and if so we clear our start time
if(player_client?.ban_cache_start == current_time)
player_client.ban_cache_start = 0
if(!query_successful)
qdel(query_build_ban_cache)
return
while(query_build_ban_cache.NextRow())
if(is_admin && !text2num(query_build_ban_cache.item[2]))
continue
ban_cache[query_build_ban_cache.item[1]] = TRUE
qdel(query_build_ban_cache)
if(QDELETED(player_client)) // Disconnected while working with the DB.
return
player_client.ban_cache = ban_cache
return ban_cache
/datum/admins/proc/ban_panel(player_key, player_ip, player_cid, role, duration = 1440, applies_to_admins, reason, edit_id, page, admin_key, global_ban = TRUE) // SKYRAT EDIT CHANGE - MULTISERVER
if (duration == BAN_PANEL_PERMANENT)
duration = null
var/panel_height = 620
if(edit_id)
panel_height = 240
var/datum/browser/panel = new(usr, "banpanel", "Banning Panel", 910, panel_height)
panel.add_stylesheet("admin_panelscss", 'html/admin/admin_panels.css')
panel.add_stylesheet("banpanelcss", 'html/admin/banpanel.css')
var/tgui_fancy = usr.client.prefs.read_preference(/datum/preference/toggle/tgui_fancy)
if(tgui_fancy) //some browsers (IE8) have trouble with unsupported css3 elements and DOM methods that break the panel's functionality, so we won't load those if a user is in no frills tgui mode since that's for similar compatability support
panel.add_stylesheet("admin_panelscss3", 'html/admin/admin_panels_css3.css')
panel.add_script("banpaneljs", 'html/admin/banpanel.js')
var/list/output = list("<form method='get' action='?src=[REF(src)]'>[HrefTokenFormField()]")
output += {"<input type='hidden' name='src' value='[REF(src)]'>
<label class='inputlabel checkbox'>Key:
<input type='checkbox' id='keycheck' name='keycheck' value='1'[player_key ? " checked": ""]>
<div class='inputbox'></div></label>
<input type='text' name='keytext' size='26' value='[player_key]'>
<label class='inputlabel checkbox'>IP:
<input type='checkbox' id='ipcheck' name='ipcheck' value='1'[isnull(duration) ? " checked" : ""]>
<div class='inputbox'></div></label>
<input type='text' name='iptext' size='18' value='[player_ip]'>
<label class='inputlabel checkbox'>CID:
<input type='checkbox' id='cidcheck' name='cidcheck' value='1' checked>
<div class='inputbox'></div></label>
<input type='text' name='cidtext' size='14' value='[player_cid]'>
<br>
<label class='inputlabel checkbox'>Use IP and CID from last connection of key
<input type='checkbox' id='lastconn' name='lastconn' value='1' [(isnull(duration) && !player_ip) || (!player_cid) ? " checked": ""]>
<div class='inputbox'></div></label>
<label class='inputlabel checkbox'>Applies to Admins
<input type='checkbox' id='applyadmins' name='applyadmins' value='1'[applies_to_admins ? " checked": ""]>
<div class='inputbox'></div></label>
<input type='submit' value='Submit'>
<br>
<div class='row'>
<div class='column left'>
Duration type
<br>
<label class='inputlabel radio'>Permanent
<input type='radio' id='permanent' name='radioduration' value='permanent'[isnull(duration) ? " checked" : ""]>
<div class='inputbox'></div></label>
<br>
<label class='inputlabel radio'>Temporary
<input type='radio' id='temporary' name='radioduration' value='temporary'[duration ? " checked" : ""]>
<div class='inputbox'></div></label>
<input type='text' name='duration' size='7' value='[duration]'>
<div class="select">
<select name='intervaltype'>
<option value='SECOND'>Seconds</option>
<option value='MINUTE' selected>Minutes</option>
<option value='HOUR'>Hours</option>
<option value='DAY'>Days</option>
<option value='WEEK'>Weeks</option>
<option value='MONTH'>Months</option>
<option value='YEAR'>Years</option>
</select>
</div>
</div>
<div class='column middle'>
Ban type
<br>
<label class='inputlabel radio'>Server
<input type='radio' id='server' name='radioban' value='server'[role == "Server" ? " checked" : ""][edit_id ? " disabled" : ""]>
<div class='inputbox'></div></label>
<br>
<label class='inputlabel radio'>Role
<input type='radio' id='role' name='radioban' value='role'[role == "Server" ? "" : " checked"][edit_id ? " disabled" : ""]>
<div class='inputbox'></div></label>
</div>
<div class='column middle'>
Severity
<br>
<label class='inputlabel radio'>None
<input type='radio' id='none' name='radioseverity' value='none'[edit_id ? " disabled" : ""]>
<div class='inputbox'></div></label>
<label class='inputlabel radio'>Medium
<input type='radio' id='medium' name='radioseverity' value='medium'[edit_id ? " disabled" : ""]>
<div class='inputbox'></div></label>
<br>
<label class='inputlabel radio'>Minor
<input type='radio' id='minor' name='radioseverity' value='minor'[edit_id ? " disabled" : ""]>
<div class='inputbox'></div></label>
<label class='inputlabel radio'>High
<input type='radio' id='high' name='radioseverity' value='high'[edit_id ? " disabled" : ""]>
<div class='inputbox'></div></label>
</div>
<div class='column right'>
Location
<br>
<label class='inputlabel radio'>Local
<input type='radio' id='servban' name='radioservban' value='local'[isnull(global_ban) ? " checked" : ""]>
<div class='inputbox'></div></label>
<br>
<label class='inputlabel radio'>Global
<input type='radio' id='servban' name='radioservban' value='global'[(global_ban) ? " checked" : ""]>
<div class='inputbox'></div></label>
</div>
<div class='column'>
Reason
<br>
<textarea class='reason' name='reason' maxlength='[MAX_REASON_LENGTH]'>[reason]</textarea>
</div>
</div>
"}
if(edit_id)
output += /* SKYRAT EDIT CHANGE - MULTISERVER */{"<label class='inputlabel checkbox'>Mirror edits to matching bans
<input type='checkbox' id='mirroredit' name='mirroredit' value='1'>
<div class='inputbox'></div></label>
<input type='hidden' name='editid' value='[edit_id]'>
<input type='hidden' name='oldkey' value='[player_key]'>
<input type='hidden' name='oldip' value='[player_ip]'>
<input type='hidden' name='oldcid' value='[player_cid]'>
<input type='hidden' name='oldapplies' value='[applies_to_admins]'>
<input type='hidden' name='oldduration' value='[duration]'>
<input type='hidden' name='oldreason' value='[reason]'>
<input type='hidden' name='old_globalban' value='[global_ban]'>
<input type='hidden' name='page' value='[page]'>
<input type='hidden' name='adminkey' value='[admin_key]'>
<input type='hidden' name='role' value='[role]'>
<br>
When ticked, edits here will also affect bans created with matching ckey, IP, CID and time. Use this to edit all role bans which were made at the same time.
"}
else
output += "<input type='hidden' name='roleban_delimiter' value='1'>"
//there's not always a client to use the bancache of so to avoid many individual queries from using is_banned_form we'll build a cache to use here
var/banned_from = list()
if(player_key)
var/datum/db_query/query_get_banned_roles = SSdbcore.NewQuery({"
SELECT role
FROM [format_table_name("ban")]
WHERE
ckey = :player_ckey AND
role <> 'server'
AND unbanned_datetime IS NULL
AND (expiration_time IS NULL OR expiration_time > NOW())
"}, list("player_ckey" = ckey(player_key)))
if(!query_get_banned_roles.warn_execute())
qdel(query_get_banned_roles)
return
while(query_get_banned_roles.NextRow())
banned_from += query_get_banned_roles.item[1]
qdel(query_get_banned_roles)
var/break_counter = 0
output += "<div class='row'>"
for(var/datum/job_department/department as anything in SSjob.joinable_departments)
var/label_class = department.label_class
var/department_name = department.department_name
output += "<div class='column'><label class='rolegroup [label_class]'>[tgui_fancy ? "<input type='checkbox' name='[label_class]' class='hidden' onClick='header_click_all_checkboxes(this)'>" : ""] \
[department_name]</label><div class='content'>"
for(var/datum/job/job_datum as anything in department.department_jobs)
if(break_counter > 0 && (break_counter % 3 == 0))
output += "<br>"
break_counter++
var/job_name = job_datum.title
if(length(job_datum.departments_list) > 1) //This job is in multiple departments, so we need to check all the boxes.
// Clicking this will also toggle all the other boxes, minus this one.
var/department_index = job_datum.departments_list.Find(department.type)
if(!department_index)
stack_trace("Failed to find a department index for [department.type] in the departments_list of [job_datum.type]")
output += {"<label class='inputlabel checkbox'>[job_name]
<input type='checkbox' id='[job_name]_[department_index]' name='[job_name]' class='[label_class]' value='1'[tgui_fancy ? " onClick='toggle_other_checkboxes(this, \"[length(job_datum.departments_list)]\", \"[department_index]\")'" : ""]>
<div class='inputbox[(job_name in banned_from) ? " banned" : ""]'></div></label>
"}
else
output += {"<label class='inputlabel checkbox'>[job_name]
<input type='checkbox' name='[job_name]' class='[label_class]' value='1'>
<div class='inputbox[(job_name in banned_from) ? " banned" : ""]'></div></label>
"}
output += "</div></div>"
break_counter = 0
var/list/other_job_lists = list(
"Abstract" = list("Appearance", "Emote", "Deadchat", "OOC", "Urgent Adminhelp"),
)
for(var/department in other_job_lists)
output += "<div class='column'><label class='rolegroup [ckey(department)]'>[tgui_fancy ? "<input type='checkbox' name='[department]' class='hidden' onClick='header_click_all_checkboxes(this)'>" : ""][department]</label><div class='content'>"
break_counter = 0
for(var/job in other_job_lists[department])
if(break_counter > 0 && (break_counter % 3 == 0))
output += "<br>"
output += {"<label class='inputlabel checkbox'>[job]
<input type='checkbox' name='[job]' class='[department]' value='1'>
<div class='inputbox[(job in banned_from) ? " banned" : ""]'></div></label>
"}
break_counter++
output += "</div></div>"
var/list/long_job_lists = list(
"Ghost and Other Roles" = list(
ROLE_PAI,
ROLE_BOT,
ROLE_BRAINWASHED,
ROLE_DEATHSQUAD,
ROLE_DRONE,
ROLE_LAVALAND,
ROLE_MIND_TRANSFER,
ROLE_POSIBRAIN,
ROLE_SENTIENCE,
),
"Antagonist Positions" = list(
ROLE_ABDUCTOR,
ROLE_ALIEN,
ROLE_BLOB,
ROLE_BROTHER,
ROLE_CHANGELING,
ROLE_CULTIST,
ROLE_HERETIC,
ROLE_HIVE,
ROLE_MALF,
ROLE_NINJA,
ROLE_OPERATIVE,
ROLE_OVERTHROW,
ROLE_REV,
ROLE_REVENANT,
ROLE_REV_HEAD,
ROLE_SENTIENT_DISEASE,
ROLE_SPIDER,
ROLE_SYNDICATE,
ROLE_TRAITOR,
ROLE_WIZARD,
ROLE_BORER, //SKYRAT EDIT
ROLE_ASSAULT_OPERATIVE, //SKYRAT EDIT
),
"Skyrat Ban Options" = list(
BAN_PACIFICATION,
BAN_DONOTREVIVE,
BAN_RESPAWN,
BAN_MOB_CONTROL,
BAN_GHOST_ROLE_SPAWNER,
BAN_GHOST_TAKEOVER,
BAN_EORG,
BAN_ANTAGONIST,
BAN_OPFOR,
BAN_LOOC, //SKYRAT ADDITION - LOOC muting again
),//SKYRAT EDIT ADDITION - EXTRA_BANS
)
for(var/department in long_job_lists)
output += "<div class='column'><label class='rolegroup long [ckey(department)]'>[tgui_fancy ? "<input type='checkbox' name='[department]' class='hidden' onClick='header_click_all_checkboxes(this)'>" : ""][department]</label><div class='content'>"
break_counter = 0
for(var/job in long_job_lists[department])
if(break_counter > 0 && (break_counter % 10 == 0))
output += "<br>"
output += {"<label class='inputlabel checkbox'>[job]
<input type='checkbox' name='[job]' class='[department]' value='1'>
<div class='inputbox[(job in banned_from) ? " banned" : ""]'></div></label>
"}
break_counter++
output += "</div></div>"
output += "</div>"
output += "</form>"
panel.set_content(output.Join())
panel.open()
/datum/admins/proc/ban_parse_href(list/href_list)
if(!check_rights(R_BAN))
return
if(!SSdbcore.Connect())
to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
return
var/list/error_state = list()
var/player_key
var/ip_check = FALSE
var/player_ip
var/cid_check = FALSE
var/player_cid
var/use_last_connection = FALSE
var/applies_to_admins = FALSE
var/global_ban = FALSE // SKYRAT EDIT ADDITION - MULTISERVER
var/duration
var/interval
var/severity
var/reason
var/mirror_edit
var/edit_id
var/old_key
var/old_ip
var/old_cid
var/old_applies
var/old_globalban // SKYRAT EDIT ADDITION - MULTISERVER
var/page
var/admin_key
var/list/changes = list()
var/list/roles_to_ban = list()
if(href_list["keycheck"])
player_key = href_list["keytext"]
if(!player_key)
error_state += "Key was ticked but none was provided."
if(href_list["ipcheck"])
ip_check = TRUE
if(href_list["cidcheck"])
cid_check = TRUE
if(href_list["lastconn"])
if(player_key)
use_last_connection = TRUE
else
if(ip_check)
player_ip = href_list["iptext"]
if(!player_ip && !use_last_connection)
error_state += "IP was ticked but none was provided."
if(cid_check)
player_cid = href_list["cidtext"]
if(!player_cid && !use_last_connection)
error_state += "CID was ticked but none was provided."
if(!use_last_connection && !player_ip && !player_cid && !player_key)
error_state += "At least a key, IP or CID must be provided."
if(use_last_connection && !ip_check && !cid_check)
error_state += "Use last connection was ticked, but neither IP nor CID was."
if(href_list["applyadmins"])
applies_to_admins = TRUE
switch(href_list["radioservban"]) // SKYRAT EDIT ADDITION BEGIN - MULTISERVER
if("local")
global_ban = FALSE
if("global")
global_ban = TRUE // SKYRAT EDIT ADDITION END - MULTISERVER
switch(href_list["radioduration"])
if("permanent")
duration = null
if("temporary")
duration = href_list["duration"]
interval = href_list["intervaltype"]
if(!duration)
error_state += "Temporary ban was selected but no duration was provided."
else
error_state += "No duration was selected."
reason = href_list["reason"]
if(!reason)
error_state += "No reason was provided."
if(length(reason) > MAX_REASON_LENGTH)
error_state += "Reason cannot be more than [MAX_REASON_LENGTH] characters."
if(href_list["editid"])
edit_id = href_list["editid"]
if(href_list["mirroredit"])
mirror_edit = TRUE
old_key = href_list["oldkey"]
old_ip = href_list["oldip"]
old_cid = href_list["oldcid"]
old_globalban = href_list["old_globalban"] // SKYRAT EDIT ADDITION - MULTISERVER
page = href_list["page"]
admin_key = href_list["adminkey"]
if(player_key != old_key)
changes += list("Key" = "[old_key] to [player_key]")
if(global_ban != old_globalban) // SKYRAT EDIT ADDITION - MULTISERVER
changes += list("Ban Location" = "[old_globalban] to [global_ban]") // SKYRAT EDIT ADDITION - MULTISERVER
if(player_ip != old_ip)
changes += list("IP" = "[old_ip] to [player_ip]")
if(player_cid != old_cid)
changes += list("CID" = "[old_cid] to [player_cid]")
old_applies = text2num(href_list["oldapplies"])
if(applies_to_admins != old_applies)
changes += list("Applies to admins" = "[old_applies] to [applies_to_admins]")
if(duration != href_list["oldduration"])
changes += list("Duration" = "[href_list["oldduration"]] MINUTE to [duration] [interval]")
if(reason != href_list["oldreason"])
changes += list("Reason" = "[href_list["oldreason"]]<br>to<br>[reason]")
if(!changes.len)
error_state += "No changes were detected."
roles_to_ban += href_list["role"]
else
severity = href_list["radioseverity"]
if(!severity)
error_state += "No severity was selected."
switch(href_list["radioban"])
if("server")
roles_to_ban += "Server"
if("role")
href_list.Remove("Command", "Security", "Engineering", "Medical", "Science", "Supply", "Silicon", "Abstract", "Service", "Ghost and Other Roles", "Antagonist Positions") //remove the role banner hidden input values
var/delimiter_pos = href_list.Find("roleban_delimiter")
if(href_list.len == delimiter_pos)
error_state += "Role ban was selected but no roles to ban were selected."
else if(delimiter_pos == 0)
error_state += "roleban_delimiter not found in href. Report this to coders."
else
href_list.Cut(1, delimiter_pos+1)//remove every list element before and including roleban_delimiter so we have a list of only the roles to ban
for(var/key in href_list) //flatten into a list of only unique keys
roles_to_ban |= key
else
error_state += "No ban type was selected."
if(error_state.len)
to_chat(usr, span_danger("Ban not [edit_id ? "edited" : "created"] because the following errors were present:\n[error_state.Join("\n")]"), confidential = TRUE)
return
if(edit_id)
edit_ban(edit_id, player_key, ip_check, player_ip, cid_check, player_cid, use_last_connection, applies_to_admins, duration, interval, reason, global_ban, mirror_edit, old_key, old_ip, old_cid, old_applies, page, admin_key, changes, roles_to_ban[1] == "Server") // SKYRAT EDIT CHANGE - MULTISERVER
else
create_ban(player_key, ip_check, player_ip, cid_check, player_cid, use_last_connection, applies_to_admins, duration, interval, severity, reason, global_ban, roles_to_ban) // SKYRAT EDIT CHANGE - MULTISERVER
/datum/admins/proc/create_ban(player_key, ip_check, player_ip, cid_check, player_cid, use_last_connection, applies_to_admins, duration, interval, severity, reason, global_ban, list/roles_to_ban) // SKYRAT EDIT CHANGE - MULTISERVER
if(!check_rights(R_BAN))
return
if(!SSdbcore.Connect())
to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
return
var/player_ckey = ckey(player_key)
if(player_ckey)
var/datum/db_query/query_create_ban_get_player = SSdbcore.NewQuery({"
SELECT byond_key, INET_NTOA(ip), computerid FROM [format_table_name("player")] WHERE ckey = :player_ckey
"}, list("player_ckey" = player_ckey))
if(!query_create_ban_get_player.warn_execute())
qdel(query_create_ban_get_player)
return
if(query_create_ban_get_player.NextRow())
player_key = query_create_ban_get_player.item[1]
if(use_last_connection)
if(ip_check)
player_ip = query_create_ban_get_player.item[2]
if(cid_check)
player_cid = query_create_ban_get_player.item[3]
else
if(use_last_connection)
if(tgui_alert(usr, "[player_key]/([player_ckey]) has not been seen before, unable to use IP and CID from last connection. Are you sure you want to create a ban for them?", "Unknown key", list("Yes", "No", "Cancel")) != "Yes")
qdel(query_create_ban_get_player)
return
else
if(tgui_alert(usr, "[player_key]/([player_ckey]) has not been seen before, are you sure you want to create a ban for them?", "Unknown key", list("Yes", "No", "Cancel")) != "Yes")
qdel(query_create_ban_get_player)
return
qdel(query_create_ban_get_player)
var/admin_ckey = usr.client.ckey
if(applies_to_admins && !can_place_additional_admin_ban(admin_ckey))
return
var/admin_ip = usr.client.address
var/admin_cid = usr.client.computer_id
duration = text2num(duration)
if (!(interval in list("SECOND", "MINUTE", "HOUR", "DAY", "WEEK", "MONTH", "YEAR")))
interval = "MINUTE"
var/time_message = "[duration] [LOWER_TEXT(interval)]" //no DisplayTimeText because our duration is of variable interval type
if(duration > 1) //pluralize the interval if necessary
time_message += "s"
var/is_server_ban = (roles_to_ban[1] == "Server")
var/note_reason = "Banned from [is_server_ban ? "the server" : " Roles: [roles_to_ban.Join(", ")]"] [isnull(duration) ? "permanently" : "for [time_message]"] - [reason]"
var/list/clients_online = GLOB.clients.Copy()
var/list/admins_online = list()
for(var/client/C in clients_online)
if(C.holder) //deadmins aren't included since they wouldn't show up on adminwho
admins_online += C
var/who = clients_online.Join(", ")
var/adminwho = admins_online.Join(", ")
var/kn = key_name(usr)
var/kna = key_name_admin(usr)
var/special_columns = list(
"bantime" = "NOW()",
"server_ip" = "INET_ATON(?)",
"ip" = "INET_ATON(?)",
"a_ip" = "INET_ATON(?)",
"expiration_time" = "IF(? IS NULL, NULL, NOW() + INTERVAL ? [interval])"
)
var/sql_ban = list()
for(var/role in roles_to_ban)
sql_ban += list(list(
"server_name" = CONFIG_GET(string/serversqlname),// SKYRAT EDIT CHANGE - MULTISERVER
"server_ip" = world.internet_address || 0,
"server_port" = world.port,
"round_id" = GLOB.round_id,
"role" = role,
"global_ban" = global_ban, // SKYRAT EDIT CHANGE - MULTISERVER
"expiration_time" = duration,
"applies_to_admins" = applies_to_admins,
"reason" = reason,
"ckey" = player_ckey || null,
"ip" = player_ip || null,
"computerid" = player_cid || null,
"a_ckey" = admin_ckey,
"a_ip" = admin_ip || null,
"a_computerid" = admin_cid,
"who" = who,
"adminwho" = adminwho
))
if(!SSdbcore.MassInsert(format_table_name("ban"), sql_ban, warn = TRUE, special_columns = special_columns))
return
var/target = ban_target_string(player_key, player_ip, player_cid)
var/msg = "has created a [global_ban ? "global" : "local"] [isnull(duration) ? "permanent" : "temporary [time_message]"] [applies_to_admins ? "admin " : ""][is_server_ban ? "server ban" : "role ban from [roles_to_ban.len] roles"] for [target]." // SKYRAT EDIT CHANGE - MULTISERVER
log_admin_private("[kn] [msg][is_server_ban ? "" : " Roles: [roles_to_ban.Join(", ")]"] Reason: [reason]")
message_admins("[kna] [msg][is_server_ban ? "" : " Roles: [roles_to_ban.Join("\n")]"]\nReason: [reason]")
if(applies_to_admins)
send2adminchat("BAN ALERT","[kn] [msg]")
if(player_ckey)
create_message("note", player_ckey, admin_ckey, note_reason, null, null, 0, 0, null, 0, severity)
var/player_ban_notification = span_boldannounce("You have been [applies_to_admins ? "admin " : ""]banned by [usr.client.key] from [is_server_ban ? "the server" : " Roles: [roles_to_ban.Join(", ")]"].\nReason: [reason]</span><br>[span_danger("This ban is [isnull(duration) ? "permanent." : "temporary, it will be removed in [time_message]."] The round ID is [GLOB.round_id].")]")
var/other_ban_notification = span_boldannounce("Another player sharing your IP or CID has been banned by [usr.client.key] from [is_server_ban ? "the server" : " Roles: [roles_to_ban.Join(", ")]"].\nReason: [reason]</span><br>[span_danger("This ban is [isnull(duration) ? "permanent." : "temporary, it will be removed in [time_message]."] The round ID is [GLOB.round_id].")]")
notify_all_banned_players(player_ckey, player_ip, player_cid, player_ban_notification, other_ban_notification, is_server_ban, applies_to_admins)
// SKYRAT EDIT ADDITION BEGIN - EXTRA_BANS
if(BAN_PACIFICATION in roles_to_ban)
var/client/C = GLOB.directory[player_ckey]
if(ismob(C.mob))
ADD_TRAIT(C.mob, TRAIT_PACIFISM, ROUNDSTART_TRAIT)
// SKYRAT EDIT ADDITION END
//var/datum/admin_help/linked_ahelp_ticket = admin_ticket_log(player_ckey, "[kna] [msg]") // SKYRAT EDIT ORIGINAL
var/datum/admin_help/linked_ahelp_ticket = admin_ticket_log(player_ckey, "[kna] [msg]", FALSE) // SKYRAT EDIT -- Player ticket viewing
if(is_server_ban && linked_ahelp_ticket)
linked_ahelp_ticket.Resolve()
/datum/admins/proc/unban_panel(player_key, admin_key, player_ip, player_cid, page = 0)
if(!check_rights(R_BAN))
return
if(!SSdbcore.Connect())
to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
return
var/datum/browser/unban_panel = new(usr, "unbanpanel", "Unbanning Panel", 850, 600)
unban_panel.add_stylesheet("unbanpanelcss", 'html/admin/unbanpanel.css')
var/list/output = list("<div class='searchbar'>")
output += {"<form method='get' action='?src=[REF(src)]'>[HrefTokenFormField()]
<input type='hidden' name='src' value='[REF(src)]'>
Key:<input type='text' name='searchunbankey' size='18' value='[player_key]'>
Admin Key:<input type='text' name='searchunbanadminkey' size='18' value='[admin_key]'>
IP:<input type='text' name='searchunbanip' size='12' value='[player_ip]'>
CID:<input type='text' name='searchunbancid' size='10' value='[player_cid]'>
<input type='submit' value='Search'>
</form>
</div>
<div class='main'>
"}
if(player_key || admin_key || player_ip || player_cid)
var/bancount = 0
var/bansperpage = 10
page = text2num(page)
var/datum/db_query/query_unban_count_bans = SSdbcore.NewQuery({"
SELECT COUNT(id)
FROM [format_table_name("ban")]
WHERE
(:player_key IS NULL OR ckey = :player_key) AND
(:admin_key IS NULL OR a_ckey = :admin_key) AND
(:player_ip IS NULL OR ip = INET_ATON(:player_ip)) AND
(:player_cid IS NULL OR computerid = :player_cid)
"}, list(
"player_key" = ckey(player_key),
"admin_key" = ckey(admin_key),
"player_ip" = player_ip || null,
"player_cid" = player_cid || null,
))
if(!query_unban_count_bans.warn_execute())
qdel(query_unban_count_bans)
return
if(query_unban_count_bans.NextRow())
bancount = text2num(query_unban_count_bans.item[1])
qdel(query_unban_count_bans)
if(bancount > bansperpage)
output += "<b>Page: </b>"
var/pagecount = 1
var/list/pagelist = list()
while(bancount > 0)
pagelist += "<a href='?_src_=holder;[HrefToken()];unbanpagecount=[pagecount - 1];unbankey=[player_key];unbanadminkey=[admin_key];unbanip=[player_ip];unbancid=[player_cid]'>[pagecount == page ? "<b>\[[pagecount]\]</b>" : "\[[pagecount]\]"]</a>"
bancount -= bansperpage
pagecount++
output += pagelist.Join(" | ")
var/datum/db_query/query_unban_search_bans = SSdbcore.NewQuery({"
SELECT
id,
bantime,
round_id,
role,
expiration_time,
TIMESTAMPDIFF(MINUTE, bantime, expiration_time),
IF(expiration_time < NOW(), 1, NULL),
applies_to_admins,
reason,
IFNULL((
SELECT byond_key
FROM [format_table_name("player")]
WHERE [format_table_name("player")].ckey = [format_table_name("ban")].ckey
), ckey),
INET_NTOA(ip),
computerid,
IFNULL((
SELECT byond_key
FROM [format_table_name("player")]
WHERE [format_table_name("player")].ckey = [format_table_name("ban")].a_ckey
), a_ckey),
IF(edits IS NOT NULL, 1, NULL),
unbanned_datetime,
IFNULL((
SELECT byond_key
FROM [format_table_name("player")]
WHERE [format_table_name("player")].ckey = [format_table_name("ban")].unbanned_ckey
), unbanned_ckey),
unbanned_round_id
FROM [format_table_name("ban")]
WHERE
(:player_key IS NULL OR ckey = :player_key) AND
(:admin_key IS NULL OR a_ckey = :admin_key) AND
(:player_ip IS NULL OR ip = INET_ATON(:player_ip)) AND
(:player_cid IS NULL OR computerid = :player_cid)
ORDER BY id DESC
LIMIT :skip, :take
"}, list(
"player_key" = ckey(player_key),
"admin_key" = ckey(admin_key),
"player_ip" = player_ip || null,
"player_cid" = player_cid || null,
"skip" = bansperpage * page,
"take" = bansperpage,
))
if(!query_unban_search_bans.warn_execute())
qdel(query_unban_search_bans)
return
while(query_unban_search_bans.NextRow())
var/ban_id = query_unban_search_bans.item[1]
var/ban_datetime = query_unban_search_bans.item[2]
var/ban_round_id = query_unban_search_bans.item[3]
var/role = query_unban_search_bans.item[4]
var/expiration_time = query_unban_search_bans.item[5]
//we don't cast duration as num because if the duration is large enough to be converted to scientific notation by byond then the + character gets lost when passed through href causing SQL to interpret '4.321e 007' as '4'
var/duration = query_unban_search_bans.item[6]
var/expired = query_unban_search_bans.item[7]
var/applies_to_admins = text2num(query_unban_search_bans.item[8])
var/reason = query_unban_search_bans.item[9]
var/banned_player_key = query_unban_search_bans.item[10]
var/banned_player_ip = query_unban_search_bans.item[11]
var/banned_player_cid = query_unban_search_bans.item[12]
var/banning_admin_key = query_unban_search_bans.item[13]
var/edits = query_unban_search_bans.item[14]
var/unban_datetime = query_unban_search_bans.item[15]
var/unban_key = query_unban_search_bans.item[16]
var/unban_round_id = query_unban_search_bans.item[17]
var/target = ban_target_string(banned_player_key, banned_player_ip, banned_player_cid)
output += "<div class='banbox'><div class='header [unban_datetime ? "unbanned" : "banned"]'><b>[target]</b>[applies_to_admins ? " <b>ADMIN</b>" : ""] banned by <b>[banning_admin_key]</b> from <b>[role]</b> on <b>[ban_datetime]</b> during round <b>#[ban_round_id]</b>.<br>"
if(!expiration_time)
output += "<b>Permanent ban</b>."
else
output += "Duration of <b>[DisplayTimeText(text2num(duration) MINUTES)]</b>, <b>[expired ? "expired" : "expires"]</b> on <b>[expiration_time]</b>."
if(unban_datetime)
output += "<br>Unbanned by <b>[unban_key]</b> on <b>[unban_datetime]</b> during round <b>#[unban_round_id]</b>."
output += "</div><div class='container'><div class='reason'>[reason]</div><div class='edit'>"
var/un_or_reban_href
if(unban_datetime)
un_or_reban_href = "<a href='?_src_=holder;[HrefToken()];rebanid=[ban_id];applies_to_admins=[applies_to_admins];rebankey=[banned_player_key];rebanadminkey=[banning_admin_key];rebanip=[banned_player_ip];rebancid=[banned_player_cid];rebanrole=[role];rebanpage=[page]'>Reban</a>"
else
un_or_reban_href = "<a href='?_src_=holder;[HrefToken()];unbanid=[ban_id];unbankey=[banned_player_key];unbanadminkey=[banning_admin_key];unbanip=[banned_player_ip];unbancid=[banned_player_cid];unbanrole=[role];unbanpage=[page]'>Unban</a>"
output += "<a href='?_src_=holder;[HrefToken()];editbanid=[ban_id];editbankey=[banned_player_key];editbanip=[banned_player_ip];editbancid=[banned_player_cid];editbanrole=[role];editbanduration=[duration];editbanadmins=[applies_to_admins];editbanreason=[url_encode(reason)];editbanpage=[page];editbanadminkey=[banning_admin_key]'>Edit</a><br>[un_or_reban_href]"
if(edits)
output += "<br><a href='?_src_=holder;[HrefToken()];unbanlog=[ban_id]'>Edit log</a>"
output += "</div></div></div>"
qdel(query_unban_search_bans)
output += "</div>"
unban_panel.set_content(jointext(output, ""))
unban_panel.open()
/datum/admins/proc/unban(ban_id, player_key, player_ip, player_cid, role, page, admin_key)
if(!check_rights(R_BAN))
return
if(!SSdbcore.Connect())
to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
return
var/target = ban_target_string(player_key, player_ip, player_cid)
// Make sure the only input that doesn't early return is "Yes" - This is the only situation in which we want the unban to proceed.
if(tgui_alert(usr, "Please confirm unban of [target] from [role].", "Unban confirmation", list("Yes", "No")) != "Yes")
return
var/kn = key_name(usr)
var/kna = key_name_admin(usr)
var/change_message = "[usr.client.key] unbanned [target] from [role] on [SQLtime()] during round #[GLOB.round_id]<hr>"
var/datum/db_query/query_unban = SSdbcore.NewQuery({"
UPDATE [format_table_name("ban")] SET
unbanned_datetime = NOW(),
unbanned_ckey = :admin_ckey,
unbanned_ip = INET_ATON(:admin_ip),
unbanned_computerid = :admin_cid,
unbanned_round_id = :round_id,
edits = CONCAT(IFNULL(edits,''), :change_message)
WHERE id = :ban_id
"}, list("ban_id" = ban_id, "admin_ckey" = usr.client.ckey, "admin_ip" = usr.client.address, "admin_cid" = usr.client.computer_id, "round_id" = GLOB.round_id, "change_message" = change_message))
if(!query_unban.warn_execute())
qdel(query_unban)
return
qdel(query_unban)
log_admin_private("[kn] has unbanned [target] from [role].")
message_admins("[kna] has unbanned [target] from [role].")
var/client/C = GLOB.directory[player_key]
if(C)
build_ban_cache(C)
to_chat(C, span_boldannounce("[usr.client.key] has removed a ban from [role] for your key."), confidential = TRUE)
for(var/client/i in GLOB.clients - C)
if(i.address == player_ip || i.computer_id == player_cid)
build_ban_cache(i)
to_chat(i, span_boldannounce("[usr.client.key] has removed a ban from [role] for your IP or CID."), confidential = TRUE)
unban_panel(player_key, admin_key, player_ip, player_cid, page)
/// Sometimes an admin did not intend to unban a player. This proc undoes an unbanning operation by setting the unbanned_ keys in the DB back to null.
/datum/admins/proc/reban(ban_id, applies_to_admins, player_key, player_ip, player_cid, role, page, admin_key)
if(!check_rights(R_BAN))
return
if(!SSdbcore.Connect())
to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
return
var/target = ban_target_string(player_key, player_ip, player_cid)
// Make sure the only input that doesn't early return is "Yes" - This is the only situation in which we want the unban to proceed.
if(tgui_alert(usr, "Please confirm undoing of unban of [target] from [role].", "Reban confirmation", list("Yes", "No")) != "Yes")
return
if(applies_to_admins && !can_place_additional_admin_ban(usr.client.ckey))
return
var/kn = key_name(usr)
var/kna = key_name_admin(usr)
var/change_message = "[usr.client.key] re-activated ban of [target] from [role] on [SQLtime()] during round #[GLOB.round_id]<hr>"
var/datum/db_query/query_reban = SSdbcore.NewQuery({"
UPDATE [format_table_name("ban")] SET
unbanned_datetime = NULL,
unbanned_ckey = NULL,
unbanned_ip = NULL,
unbanned_computerid = NULL,
unbanned_round_id = NULL,
edits = CONCAT(IFNULL(edits,''), :change_message)
WHERE id = :ban_id
"}, list("change_message" = change_message, "ban_id" = ban_id))
if(!query_reban.warn_execute())
qdel(query_reban)
return
qdel(query_reban)
log_admin_private("[kn] has rebanned [target] from [role].")
message_admins("[kna] has rebanned [target] from [role].")
var/banned_player_message = span_boldannounce("[usr.client.key] has re-activated a removed ban from [role] for your key.")
var/banned_other_message = span_boldannounce("[usr.client.key] has re-activated a removed ban from [role] for your IP or CID.")
var/kick_banned_players = (role == "Server")
notify_all_banned_players(ckey(player_key), player_ip, player_cid, banned_player_message, banned_other_message, kick_banned_players, applies_to_admins)
unban_panel(player_key, admin_key, player_ip, player_cid, page)
/datum/admins/proc/edit_ban(ban_id, player_key, ip_check, player_ip, cid_check, player_cid, use_last_connection, applies_to_admins, duration, interval, reason, global_ban, mirror_edit, old_key, old_ip, old_cid, old_applies, admin_key, page, list/changes, is_server_ban) // SKYRAT EDIT CHANGE - MULTISERVER
if(!check_rights(R_BAN))
return
if(!SSdbcore.Connect())
to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
return
var/player_ckey = ckey(player_key)
var/bantime
if(player_ckey)
var/datum/db_query/query_edit_ban_get_player = SSdbcore.NewQuery({"
SELECT
byond_key,
(SELECT bantime FROM [format_table_name("ban")] WHERE id = :ban_id),
ip,
computerid
FROM [format_table_name("player")]
WHERE ckey = :player_ckey
"}, list("player_ckey" = player_ckey, "ban_id" = ban_id))
if(!query_edit_ban_get_player.warn_execute())
qdel(query_edit_ban_get_player)
return
if(query_edit_ban_get_player.NextRow())
player_key = query_edit_ban_get_player.item[1]
bantime = query_edit_ban_get_player.item[2]
if(use_last_connection)
if(ip_check)
player_ip = query_edit_ban_get_player.item[3]
if(cid_check)
player_cid = query_edit_ban_get_player.item[4]
else
if(use_last_connection)
if(tgui_alert(usr, "[player_key]/([player_ckey]) has not been seen before, unable to use IP and CID from last connection. Are you sure you want to edit a ban for them?", "Unknown key", list("Yes", "No", "Cancel")) != "Yes")
qdel(query_edit_ban_get_player)
return
else
if(tgui_alert(usr, "[player_key]/([player_ckey]) has not been seen before, are you sure you want to edit a ban for them?", "Unknown key", list("Yes", "No", "Cancel")) != "Yes")
qdel(query_edit_ban_get_player)
return
qdel(query_edit_ban_get_player)
if(applies_to_admins && (applies_to_admins != old_applies) && !can_place_additional_admin_ban(usr.client.ckey))
return
if (!(interval in list("SECOND", "MINUTE", "HOUR", "DAY", "WEEK", "MONTH", "YEAR")))
interval = "MINUTE"
var/list/changes_text = list()
var/list/changes_keys = list()
for(var/i in changes)
changes_text += "[i]: [changes[i]]"
changes_keys += i
var/change_message = "[usr.client.key] edited the following [jointext(changes_text, ", ")]<hr>"
var/list/arguments = list(
"duration" = duration || null,
"reason" = reason,
"applies_to_admins" = applies_to_admins,
"ckey" = player_ckey || null,
"ip" = player_ip || null,
"cid" = player_cid || null,
"change_message" = change_message,
"global_ban" = global_ban // SKYRAT EDIT ADDITION - MULTISERVER
)
var/where
if(text2num(mirror_edit))
var/list/wherelist = list("bantime = '[bantime]'")
if(old_key)
wherelist += "ckey = :old_ckey"
arguments["old_ckey"] = ckey(old_key)
if(old_ip)
wherelist += "ip = INET_ATON(:old_ip)"
arguments["old_ip"] = old_ip || null
if(old_cid)
wherelist += "computerid = :old_cid"
arguments["old_cid"] = old_cid
where = wherelist.Join(" AND ")
else
where = "id = :ban_id"
arguments["ban_id"] = ban_id
var/datum/db_query/query_edit_ban = SSdbcore.NewQuery(/* SKYRAT EDIT CHANGE - MULTISERVER */{"
UPDATE [format_table_name("ban")]
SET
expiration_time = IF(:duration IS NULL, NULL, bantime + INTERVAL :duration [interval]),
applies_to_admins = :applies_to_admins,
reason = :reason,
global_ban = :global_ban,
ckey = :ckey,
ip = INET_ATON(:ip),
computerid = :cid,
edits = CONCAT(IFNULL(edits,''), :change_message)
WHERE [where]
"}, arguments)
if(!query_edit_ban.warn_execute())
qdel(query_edit_ban)
return
qdel(query_edit_ban)
var/changes_keys_text = jointext(changes_keys, ", ")
var/kn = key_name(usr)
var/kna = key_name_admin(usr)
log_admin_private("[kn] has edited the [changes_keys_text] of a ban for [old_key ? "[old_key]" : "[old_ip]-[old_cid]"].") //if a ban doesn't have a key it must have an ip and/or a cid to have reached this point normally
message_admins("[kna] has edited the [changes_keys_text] of a ban for [old_key ? "[old_key]" : "[old_ip]-[old_cid]"].")
if(changes["Applies to admins"])
send2adminchat("BAN ALERT","[kn] has edited a ban for [old_key ? "[old_key]" : "[old_ip]-[old_cid]"] to [applies_to_admins ? "" : "not"]affect admins")
var/player_edit_message = span_boldannounce("[usr.client.key] has edited the [changes_keys_text] of a ban for your key.")
var/other_edit_message = span_boldannounce("[usr.client.key] has edited the [changes_keys_text] of a ban for your IP or CID.")
var/kick_banned_players = (is_server_ban && (changes["Key"] || changes["IP"] || changes["CID"]))
notify_all_banned_players(player_ckey, player_ip, player_cid, player_edit_message, other_edit_message, kick_banned_players, applies_to_admins)
unban_panel(player_key, null, null, null, page)
/datum/admins/proc/ban_log(ban_id)
if(!check_rights(R_BAN))
return
if(!SSdbcore.Connect())
to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
return
var/datum/db_query/query_get_ban_edits = SSdbcore.NewQuery({"
SELECT edits FROM [format_table_name("ban")] WHERE id = :ban_id
"}, list("ban_id" = ban_id))
if(!query_get_ban_edits.warn_execute())
qdel(query_get_ban_edits)
return
if(query_get_ban_edits.NextRow())
var/edits = query_get_ban_edits.item[1]
var/datum/browser/edit_log = new(usr, "baneditlog", "Ban edit log")
edit_log.set_content(edits)
edit_log.open()
qdel(query_get_ban_edits)
/datum/admins/proc/ban_target_string(player_key, player_ip, player_cid)
. = list()
if(player_key)
. += player_key
else
if(player_ip)
. += player_ip
else
. += "NULL"
if(player_cid)
. += player_cid
else
. += "NULL"
. = jointext(., "/")
/**
* Checks if the admin can place an additional admin ban.
*
* Returns FALSE if the query fails to execute.
* Returns FALSE and notifies the admin in chat if they are at their max number of admin bans already.
* Returns TRUE if an admin can place an additional admin ban.
*
* Arguments:
* * admin_ckey - The ckey of the admin who is trying to place an admin ban.
*/
/datum/admins/proc/can_place_additional_admin_ban(admin_ckey)
var/datum/db_query/query_check_adminban_count = SSdbcore.NewQuery({"
SELECT COUNT(DISTINCT bantime)
FROM [format_table_name("ban")]
WHERE
a_ckey = :admin_ckey AND
applies_to_admins = 1 AND
unbanned_datetime IS NULL AND
(expiration_time IS NULL OR expiration_time > NOW())
"}, list("admin_ckey" = admin_ckey))
if(!query_check_adminban_count.warn_execute()) //count distinct bantime to treat rolebans made at the same time as one ban
qdel(query_check_adminban_count)
return FALSE
if(query_check_adminban_count.NextRow())
var/adminban_count = text2num(query_check_adminban_count.item[1])
var/max_adminbans = MAX_ADMINBANS_PER_ADMIN
if(check_rights(R_PERMISSIONS, show_msg = FALSE) && (can_edit_rights_flags() & R_EVERYTHING) == R_EVERYTHING) //edit rights are a more effective way to check hierarchical rank since many non-headmins have R_PERMISSIONS now
max_adminbans = MAX_ADMINBANS_PER_HEADMIN
if(adminban_count >= max_adminbans)
to_chat(usr, span_danger("You've already logged [max_adminbans] admin ban(s) or more. Do not abuse this function!"), confidential = TRUE)
qdel(query_check_adminban_count)
return FALSE
qdel(query_check_adminban_count)
return TRUE
/**
* Notifies all banned players about their ban and gives them a link to appeal from the config. If it was a server ban, it also kicks them.
*
* If the banned player's ckey has a linked client on the server, it notifies them of the ban details and kicks them if it was a server ban.
* If there is anyone else sharing the banned player's IP or CID, it notifies them of the ban details and kicks them if it was a server ban.
*
* Builds the ban cache for every client impacted by the ban.
*
* Arguments:
* * banned_player_ckey - The ckey of the banned player.
* * banned_player_ip - IP address of the banned player.
* * banned_player_cid - CID of the banned player.
* * banned_player_message - The message to show to only the specifically banned player.
* * banned_other_message - The message to show to any other players who share the banned player's IP or CID.
* * kick_banned_players - TRUE if we want to kick affected players, FALSE otherwise. This should generally only be TRUE for server bans.
* * applies_to_admins - TRUE if this ban applies to admins and we may need to kick them, FALSE otherwise.
*/
/datum/admins/proc/notify_all_banned_players(banned_player_ckey, banned_player_ip, banned_player_cid, banned_player_message, banned_other_message, kick_banned_players, applies_to_admins)
var/client/player_client = GLOB.directory[banned_player_ckey]
var/appeal_url = "No ban appeal url set!"
appeal_url = CONFIG_GET(string/banappeals)
var/is_admin = FALSE
if(player_client)
build_ban_cache(player_client)
to_chat(player_client, span_boldannounce("[banned_player_message]<br><span class='danger'>To appeal this ban go to [appeal_url]"), confidential = TRUE)
if(GLOB.admin_datums[player_client.ckey] || GLOB.deadmins[player_client.ckey])
is_admin = TRUE
if(kick_banned_players && (!is_admin || (is_admin && applies_to_admins)))
qdel(player_client)
for(var/client/other_player_client in GLOB.clients - player_client)
if(other_player_client.address == banned_player_ip || other_player_client.computer_id == banned_player_cid)
build_ban_cache(other_player_client)
to_chat(other_player_client, span_boldannounce("[banned_other_message]<br><span class='danger'>To appeal this ban go to [appeal_url]"), confidential = TRUE)
if(GLOB.admin_datums[other_player_client.ckey] || GLOB.deadmins[other_player_client.ckey])
is_admin = TRUE
if(kick_banned_players && (!is_admin || (is_admin && applies_to_admins)))
qdel(other_player_client)
#undef MAX_ADMINBANS_PER_ADMIN
#undef MAX_ADMINBANS_PER_HEADMIN
#undef MAX_REASON_LENGTH