mirror of
https://github.com/corvax-team/ss14-wl.git
synced 2026-02-14 19:29:57 +01:00
* or was it * S: Awaiting Changes * Temporarily make singularity a bit harder to loose as non-antag * Fix Fluent string ID copypaste fail * Fix the component defaults * Changes + Cleanup * Reduced cost of coloured light fixtures * Bump the failsafe timer down * Increase softcap back to 80 (#33400) * Toggle clothing fix (#32826) * toggle clothing fix * some adding * Automatic changelog update * .NET 9 forward compatibility changes (#33421) This doesn't switch the projects over to .NET 9, but it does make them work on .NET 9 when we decide to switch in the future. * Fix security riot crate (#33415) * move riot crate from security to armory category * Move riot crate to armory, actually make it require armory access to unlock * Localize planet dataset names (#33398) * Localize planet names (borer) * DatasetPrototype -> LocalizedDatasetPrototype * Apply requested changes * Automatic changelog update * Remove drag & drop dropping items from containers (#32706) * Initial commit * Update based on maintainer discussion * Forgot to remove this woops * Automatic changelog update * Automatically add "Approved" to maintainer PRs (#33337) * Add an Approved labeler for maintainer PRs * Be extra safe with conditions * Crew monitoring crate updated to contain flatpacks, science access instead of engi (#33417) * Make a crew monitoring crate with flatpacks * fix image * migration * Automatic changelog update * Add emag functionality * Move some of the new singularity code into shared Hopefully without explosions yay * Fix toggle verbs (#32138) First commit Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> * Construction menu grid view (#32577) * button * implement populate grid view * tweak min width * Make grid button toggle visible * tweak min window size * fix missing recipe button when mirroring item * make grid buttons toggleable * align button texture vertically * selected grid item has plain color background * tweak window width so all buttons look good * rename select method, defer colouring * get icon better * whoops * simpler button toggle * spritesys frame0, move spritesys * delete old sprite system refs * Automatic changelog update * Automatic changelog update * Automatic changelog update * Actually make the emagging popup work properly * Move PlayerBeforeSpawnEvent and PlayerSpawnCompleteEvent to Shared * Fix imports * Automatic changelog update * Automatic changelog update * Automatic changelog update * Set airlock unlit layers as invisible (#32484) Doesn't really affect anything due to appearance bulldozing this but this aligns with their actual normal states so. * Update salvage.yml * Update cargo_vending.yml * Update salvage.yml * Update salvage.yml * Update to Robust v237.2.0 * Syndicate item fix ups (#33435) tweaks and fixes * Coal presents and chrimmas tree options. Presents no longer itemify (#33147) * Dont ensure ItemComp because it could lead to weirds, and also PickupOrDrop handles non-items already. * presents and tree * woops * reviews a * Automatic changelog update * Fix approval labeler (#33440) * Fix approval labeler * Update labeler-review.yml * Update labeler-review.yml * electrification hud * Fix admin ghosts not being able to see items in pockets or interact with them (#31076) * Fix admin ghosts not being able to see items in pouches or interact with them * fix * oops --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Automatic changelog update * Gas pipe sensors (#33128) * Initial commit * Monitored pipe node is now referenced by name * Review changes * Simplified construction * Tweaked deconstruction to match other binary atmos devices * Helper function removal * Updated attribution * Automatic changelog update * Add telegram to the server info-links (#33459) * Fix sandbox error with new HWID code. (#33461) Oops * cleanup * rename * another rename * baby proof the terminal (#33281) * baby proof the terminal * Make a couple exceptions for items that you might take with you. * alwayspoweredlights * Uncuttable cables since cablecomp is a snowflake construction system * chairs and vendors * rerun heisentests * rerun tests again * Automatic changelog update * Automatic changelog update * Automatic changelog update * Disable submit admin note button on switch to note (#33456) Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.co> * Automatic changelog update * Fix startingGear storage (#33394) * fix starting gear storage * removal of unused --------- Co-authored-by: MetalSage <metalsage.official@gmail.com> * Minor improvements & fixes to Shuttle Console UI (#31623) * Fix grids and docks being culled from display prematurely * Fix inconsistent disabling of "Undock" buttons * Add a radar icon to indicate where the controlling console is * Tidy up math Remove lots of sketchy transforms-of-transforms, which should have been as single matrix multiply. Assign proper names to matrices. Remove some redundant calculations. * Feedback * Fix door animations mispredicting if closing is interrupted (#33481) * Fix door animations mispredicting if closing is interrupted On master it will flicker states a little bit partially due to it not being predicted. Instead we'll just set it straight back to opening (no animation is ever played anyway). * no log * Automatic changelog update * Don't show drag-drop outline if climbing (#33477) It won't actually do anything. * Automatic changelog update * Ensure wires can always be cut (#32447) ensure wires are always cut * Automatic changelog update * babyproof arrivals shuttle (#33284) * babyproof arrivals shuttle * always powered lights * uncuttable cables from terminal PR. --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Automatic changelog update * Add delay to AutoOrient (#33479) It functions identically to how V1 of orientation worked and it's incredibly annoying. * Automatic changelog update * Update Credits (#33503) Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com> * Draw muzzle flash below mobs (#33465) * Draw muzzle flash below mobs * Better naming --------- Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.co> * Automatic changelog update * Allow shuttles on planets to make FTL jump (#33507) This check conflicts with an attempt to FTL from the planet before expedition ends * Automatic changelog update * fix exped caves generation (#32890) Co-authored-by: deltanedas <@deltanedas:kde.org> * Automatic changelog update * various material & ore inhands (#33342) * begin * bones + pyrotten + goliath hide inhands * Update Resources/Prototypes/Entities/Objects/Materials/materials.yml Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Resources/Textures/Objects/Materials/materials.rsi/meta.json Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Resources/Textures/Objects/Materials/materials.rsi/meta.json Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Resources/Textures/Objects/Materials/materials.rsi/meta.json Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * pyrottOn --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Delete HOS headset from warden's locker (#33234) * add headset * Add icons * Meta change * fix * Revert + delete headset from locker * RCD icons resprite (#2800) * fix airlocks inconsistently auto-closing after unbolting (#33524) fix door auto close timer * Automatic changelog update * baby proof the terminal (#33281) * baby proof the terminal * Make a couple exceptions for items that you might take with you. * alwayspoweredlights * Uncuttable cables since cablecomp is a snowflake construction system * chairs and vendors * rerun heisentests * rerun tests again * babyproof arrivals shuttle (#33284) * babyproof arrivals shuttle * always powered lights * uncuttable cables from terminal PR. --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * [BUGFIX] "Ghost" in the lobby lets you see the whole chat (#33529) * fix bug, in ghost command lobby * fix * Fix build * Automatic changelog update * removed obsolete netmessage creator (#33542) removed opsolete netmessage createor * Fix RA0003 warning for ChatBox (#33531) * Shark plushies now goes rawr on hit. (#33540) Shark goes rawr more * Automatic changelog update * Turn off PointLights on VendingMachines when broken or off. (#33513) The light itself should already turn off due to `LitOnPowered` component, but the broken state of a VendingMachine did not. Fixes #33382 * Automatic changelog update * Adds more diona names (#33066) * adds more diona names * more stuff * AHHHHHHHHHHHHHHHHHHHHHHH * further additions * removes depression + adds comment * fixes + remove some weird stuff + more stuff * remove haste * minor AI cleanup (#33555) * minor cleanup * to * Simplify separated screen top menu (#33047) * Automatic changelog update * [Maps] Maus Winter Update (#2803) * [MAPS] Paper tweak (#2804) * Automatic changelog update * Ghost role drop-down alignment (#33397) * dropdown shares margin width with children * removed dependency that rider added for some reason * reduced vertical margin from 8 to 2 * Greytide Virus station event (#33547) * proof of concept * full implementation * I commited a crime * t * min players increase * Make shuttle airlocks not snapcardinals (#33557) * Make shuttle airlocks not snapcardinals * Update Resources/Prototypes/Entities/Structures/Doors/Airlocks/shuttle.yml Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Resources/Prototypes/Entities/Structures/Doors/Airlocks/shuttle.yml Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --------- Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.co> Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Automatic changelog update * [MAPS] Avrite winter (#2806) * Added two N2 lockers to Reach (#33409) Co-authored-by: dylanstrategie <188926747+dylanstrategie@users.noreply.github.com~> * Hotfix the randomly occurring DeleteAllThenGhost test failures (#33582) * clear mindrole on component shutdown * let it go * Fix space ambient music (#33594) * Automatic changelog update * Fix windoor and high security door not showing electrocution HUD (#33551) * Automatic changelog update * [Maps] Awesome new god update (#2811) * [Maps] Avrite minor change (#2810) * Automatic changelog update * Removes burnt tiles (#33422) remove burnt tiles * Parallax pack 2 (#2813) * Added Oppenhopper poster to the game. (#33588) * o7 * - * oppenhopper v2 * Update Resources/Prototypes/Entities/Structures/Wallmounts/Signs/posters.yml * Update Resources/Prototypes/Entities/Structures/Wallmounts/Signs/posters.yml --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Fix version for electril_grill meta.json (#33611) fix fixing the version thing in meta json for electril grill textures Co-authored-by: Arthur Kustenko <arthur.kustenko@learnet.se> * Fix gauze eyepatch flying pixel (#33564) boo * Fix swapped uniform printer east/west sprites (#33442) Fix uniform printer sprites rotating the wrong way * Makes knives fly straight when thrown (#33615) Gives knives a thrown angle * Light verb is now predicted (#33622) Fix * Automatic changelog update * Update `Content.PatreonParser` to use `net8.0` `TargetFramework` (#33559) Update Content.PatreonParse to use net8.0 targetframework * Approval labeler fix electric boogaloo (#33633) Nik told me to not name this "nya" * Fix gender, maybe (#33631) Co-authored-by: Alpha-Two <alpha2.5232@gmail.com> * Display GPS coordinates on their own line (#33625) * Automatic changelog update * Remove grasshopper from the panic bunker message (#33638) Can be retargetted to stable if preferred, which will then be a hotfix * Add admin log for ghost warping (#33636) * Automatic changelog update * Rename nitrogen internals crate (#33545) * rename nitrogen internals crate * nitrogen internals crate description * migrate CrateNitrogenInternals ID * Automatic changelog update * Cleanup some Client atmos systems (#33634) * Cleanup `ScrubberControl.xaml.cs` * Minor cleanups * Another pile of minor cleanups * Apply requested changes * Rename "which" into "bound". Add whitespace after "if" * Fix for arrivals deleting nuke (#33659) Adds FTLSmashImmune to nuke prototype to stop it from arrival smashing Adds a comment in FasterThanLight to indicate where the FTLSmashImmuneComponent is checked Co-authored-by: aa5g21 <aa5g21@soton.ac.uk> * Automatic changelog update * add locale to Shuttle Console Map tab (#33651) fixed * Automatic changelog update * fix PermanentBlindnessComponent to be not so permanent (#33292) * adjust min blindness back to 0 when PermanentBlindnessComponent is removed * mapinit changes * remove OnRemove, move changes to OnShutdown * goodbye event * dependency removal * final adjustment --------- Co-authored-by: lunarcomets <luanrcomets2@gmail,com> * Cog/Marathon: Airlock fixes (#33621) * Fixes the TEG airlock on marathon * Fixes the atmos external airlock on cog * Automatic changelog update * Revert "Rebalancing zombie mode chances" (#2818) * Automatic changelog update * [Maps] Silly Winter Update (#2809) * [Maps] Pilgrim update #7 — Winter (#2821) * Fix for handcuffing someone more than once (#33646) * Fix for over-cuffing someone * comment * Automatic changelog update * Fix for inspecting entities in hand (#33642) Actually inspect entity in hands * Automatic changelog update * Fix for inspecting entities in the stripping window (#33644) Handle it * Automatic changelog update * Fix borgs not getting names on roundstart (#33578) * Fix borgs not getting names on roundstart * Glory to the NT * Allow riggable to take in multiple reagents * Revert --------- Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.co> * Automatic changelog update * Require Wield To Activate Double-bladed ESword (#32869) * Add MeleeRequiresWield component * Prevent world activation * Automatic changelog update * Fix makeghostroleraffle command where 4 arguments (#31836) Fix makeghostroleraffle command with 4 arguments * Automatic changelog update * Update Credits (#33670) Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com> * Fix formatting IDE0055 warnings in VS Code (#33669) * Replace obsolete GetTilesIntersecting methods (#32455) * Remove usage of obsolete GetTilesIntersecting round 1 * Oop wrong uids * Remove usage of obsolete GetLocalTilesIntersecting round 2 * Remove usage of obsolete GetLocalTilesIntersecting final round * weh * Fix using `SharedMapSystem` in `StencilOverlay` * delta winter (#2824) * Update Snowasis (#33364) * Update Snowasis * Appease test gods * Woops had the leftward animation backwards. Fixed. * add santa suits and envelopes to chapel * It looks worse unscaled but whatever. * fix ghost role * examines * Automatic changelog update * [MAPS] Paper Cristmass update (#2822) * Fix wrong system usage (#33679) Fix wrong system using * resprite new year nuke (#2825) * Fix BuckleSystem always marking InteractHandEvent as Handled (#33602) * Add check before marking event as handled * Update Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Cleanup --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Automatic changelog update * Touching up Bagel Security Brig (#33680) * Touching up Bagel Security Brig * Added a few missing door names * Last door name I promise * Address feedback * Forgot to delete old shutter button --------- Co-authored-by: dylanstrategie <188926747+dylanstrategie@users.noreply.github.com~> * [Maps] Tushkan update #3 — Winter (#2826) * Map pool tweak (#2827) * Fixed ghost role rules for some syndicate familiars (#32457) * fix ghost role rules for some syndicate familiars * change from monkey rules to Team Antag rules. * Resolve reviews + Fix rules for LoneOp and Xenos * ghostrole rules --------- Co-authored-by: JIPDawg <JIPDawg93@gmail.com> Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com> * Automatic changelog update * Fix lobby countdown not showing hours (#33685) * New Low-Mid Pop Station - Amber Station (#33441) * Initial Commit with two departments done * Checkpointing work * Added most of service and science * Started work on medbay * Modified TEG setup and set up atmos pipes for engineering * Re-added medbay, added chapel and janitorial, started routing disposals and generally the final layout of the map is coming together * Plenty of additions, too many to list in a commit message * Major update * Nearly done with the map, just have to do decals and emergency lights * Added all decals, only thing missing is department signs * Toned back the dirt decals a bunch, added hallway signs and other decorations * Finishing touches on the map before testing. * Fixed invalids * Renamed the station to Amber, and made a couple adjustments * Completely redid cargo, added maints around sec * Added Amber to the map post init integration test * Many small updates addressing issues. * Ran a script to update all the camera names, also ran fixgridatmos, fixrotations, tilewalls, and variantize * Started addressing some of the issues brough up by Emisse and others * Addressed all issues mentioned by reviewers. Added mail system. * Wrapped up meeting all the issues raised by reviewers, also did a bunch of testing and resolved issues found during those tests * Addressed additional requested changes, and nabbed some ship designs from Frontier. Redesigned south east maints and surrounding meteor nets based off of those designs * Making a couple more requested changes * Reduced the amount of Nitrogen Closets, made several small adjustments. Ready for review again! * Removed invalids! * web edit lmao --------- Co-authored-by: Emisse <99158783+Emisse@users.noreply.github.com> * Automatic changelog update * Makes kukri not use combat knife's storage sprite (#33661) * Adds a morgue locked maints airlock (#33693) Co-authored-by: Velcroboy <velcroboy333@hotmail.com> * Juiceable slimeballs (#33660) adds extractable component to slimeball prototype * Add missing nacho recipes (#33637) * Nacho recipes, nutrition, and trash In meal_recipes.yml: Add recipes for Nachos, Cheesy Nachos, and Cuban Nachos. In meals.yml: Add a solutions container to regular nachos so it lines up with the others, and add a small plate as trash to each one. * Volumes, nutritional tweaks, and flavor In meals.yml: Lowered maxVol on nachos to leave 5 units of space. Nachos nutriment and vitamin lowered for costing so little. A cheese wedge is 3.75 nutriment, so cheesy having 4 more total nutritional value lines up nicely to me. Cuban nachos had too little volume to fit its reagents in the first place, so increased maxVol by 10. Chili peppers are 4 nutri / 4 vitamin and ketchup has a touch of tomato, so bumped to 8 and 5. Capsaicin lowered partly to make it an even 20u. Ketchup dilutes I guess. Also they don't have cheese so I changed the flavor profile to tomato instead. In meal_recipes.yml: Cuban Nachos recipe tweaked to require one less pepper. Given above numbers, this fits the nutritional value imo. * Automatic changelog update * Amber Station Seasonal Update (#33698) Made some modifications requested by reviewers prior to and after map merge, also added holiday decorations! * [Maps] Pilgrim update #8 & Silly tweak (#2828) * Amber Improvements (#33707) * Various changes (see PR) * variantized, fixedgridatmos, tiledwalls * Fix sinks and toilets not draining (#33691) * Fix AutoDrain Per the system comments, AutoDrain is designed to automatically move puddles into the drain (like a floor drain). Drains without AutoDrain are still supposed to gradually empty the buffer, but not remove puddles (like sinks and toilets). However, a logic error in the original implementation causes drains with AutoDrain set to false to simply not work. Hence sinks never emptied. * Update documentation * Automatic changelog update * Fix layout on wires UI (#33714) Layout would break for machines with >6 lights because the column count was hardcoded. Uncap the UI width and fix the rows count instead. Lights with less than 4 characters of text weren't aligned right, now they are. * Automatic changelog update * Fix for towels not having a cooldown for cleaning (#33700) Added a delay to cleaning with the towel * Add cooldown to buttons in borg's laws UI (#31490) * Adds Store on Collide and Wand of the Locker (#33710) * Adds wand of locker and locker projectile * Adds IsOpen method to check if storage is open * Adds store on collide * Adds Store On Collide to Wizard Locker * Adds Lock API * Adds locking support * Adds resist override and custom visual layers * Fixes decursed states, adds comment for a future visualizer * adds locker wand visuals and descriptions * shrinks locker radius, moves TODO for throw support * Adds whitelist and moves storage and lock logic into their own methods * Adds support to disable store on collide after the first open. Fixes prediction issues with disabling. * Adds wand of locker to the grimoire * Adds wizard access prototype * Adds Wizard to universal access * Moves Lock on collide to on collide method * Comments * Changes layer order * Fixes prediction issues when locking. * Adds Wiz access to universal ID * Automatic changelog update * Update locale * Fix upstream * update server configs (#2799) * Fix displacements map for female vulp and reptilian (#2832) Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com> * Translate Upstream #2748 (#2837) Co-authored-by: lapatison <100279397+lapatison@users.noreply.github.com> * Перевод Upstream #2830 (#2836) Co-authored-by: lapatison <lapatisonsocial@gmail.com> Co-authored-by: NotSoDamn <75203942+NotSoDana@users.noreply.github.com> Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com> Co-authored-by: cfif126 <94059374+cfif126@users.noreply.github.com> * translate borer names (#2835) * [Localize] Poster 52 (#2840) * Translate new diona names (#2839) Co-authored-by: lapatison <100279397+lapatison@users.noreply.github.com> * [Resprite] Medical uniforms & rollerbeds (#2802) * Automatic changelog update * Revert "new year lobbyscreens removed" (#2831) * Surgeon spawnpoint (#2841) Added to support map in corvax next Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com> * fix nukedisk chances (#2745) * Adds surgeon spawnpoint on maps (#2845) * Winter stuff tweak (#2833) * [MAPS] Silly Christmass Date (#2854) * [MAPS] Paper Christmass Date (#2853) * [MAPS] Avrite New Year update (#2852) * [Maps] Tushkan new year update (#2851) * [Maps] Pilgrim new year update (#2850) * [Maps] Maus new year update (#2849) * [Maps] Awesome new year update (#2848) * [Maps] Outpost return (#2847) Co-authored-by: kvant8 <ar4477023rr.@gmail.com> Co-authored-by: Ko4ergaPunk <62609550+Ko4ergaPunk@users.noreply.github.com> * [Maps] Astra new year update (#2844) * Update corvax_delta.yml (#2856) * [Maps] Pilgrim hotfix #2 (#2860) * Resprite botany food (#2858) * Fix spacelaw loadout (#2861) * Make Jolene TTS voice female (#2834) * Update configs (#2855) * New Year Music (#2829) * Revert "Удаление лимита времени для доступа к стартовым ролям." (#2652) * Automatic changelog update * Automatic changelog update * [Maps] Glacier new year update (#2864) --------- Co-authored-by: ScarKy0 <scarky0@onet.eu> Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> Co-authored-by: Saphire <lattice@saphi.re> Co-authored-by: Justice League <the.justice.league.of.canada@gmail.com> Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com> Co-authored-by: ArZarLordOfMango <96249677+ArZarLordOfMango@users.noreply.github.com> Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com> Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com> Co-authored-by: Plykiya <58439124+Plykiya@users.noreply.github.com> Co-authored-by: MilenVolf <63782763+MilenVolf@users.noreply.github.com> Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> Co-authored-by: SlamBamActionman <slambamactionman@gmail.com> Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com> Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: qwerltaz <69696513+qwerltaz@users.noreply.github.com> Co-authored-by: DrSmugleaf <drsmugleaf@gmail.com> Co-authored-by: MissKay1994 <15877268+MissKay1994@users.noreply.github.com> Co-authored-by: ThatGuyUSA <thatguyusa123@gmail.com> Co-authored-by: IProduceWidgets <107586145+IProduceWidgets@users.noreply.github.com> Co-authored-by: Vasilis <vasilis@pikachu.systems> Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Co-authored-by: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com> Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> Co-authored-by: c4llv07e <igor@c4llv07e.xyz> Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.com> Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.co> Co-authored-by: MetalSage <74924875+MetalSage@users.noreply.github.com> Co-authored-by: MetalSage <metalsage.official@gmail.com> Co-authored-by: eoineoineoin <github@eoinrul.es> Co-authored-by: goet <6637097+goet@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com> Co-authored-by: mubururu_ <139181059+muburu@users.noreply.github.com> Co-authored-by: kosticia <kosticia46@gmail.com> Co-authored-by: SonicHDC <100022571+SonicHDC@users.noreply.github.com> Co-authored-by: Schrödinger <132720404+Schrodinger71@users.noreply.github.com> Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com> Co-authored-by: Ben <benjaminevanownby@gmail.com> Co-authored-by: Nikolai Korolev <CrafterKolyan@mail.ru> Co-authored-by: Minemoder5000 <minemoder50000@gmail.com> Co-authored-by: Niels Huylebroeck <red15@users.noreply.github.com> Co-authored-by: Flareguy <78941145+Flareguy@users.noreply.github.com> Co-authored-by: Meguneri <163569304+Meguneri@users.noreply.github.com> Co-authored-by: TiFeRi <57865696+XsenonDash@users.noreply.github.com> Co-authored-by: IanComradeBot <96892333+IanComradeBot@users.noreply.github.com> Co-authored-by: Intoxicating-Innocence <188202277+Intoxicating-Innocence@users.noreply.github.com> Co-authored-by: CaptainMaru <124701378+CaptainMaru@users.noreply.github.com> Co-authored-by: dylanstrategie <188926747+dylanstrategie@users.noreply.github.com> Co-authored-by: dylanstrategie <188926747+dylanstrategie@users.noreply.github.com~> Co-authored-by: Stubaretka24 <143966631+Stubaretka24@users.noreply.github.com> Co-authored-by: AlexUm <159550239+AlexUm418@users.noreply.github.com> Co-authored-by: Arthur Kustenko <arthur.kustenko@learnet.se> Co-authored-by: Mifia <xzx._@outlook.com> Co-authored-by: SpaceManiac <tad@platymuus.com> Co-authored-by: SpaceRox1244 <138547931+SpaceRox1244@users.noreply.github.com> Co-authored-by: Alpha-Two <92269094+Alpha-Two@users.noreply.github.com> Co-authored-by: Alpha-Two <alpha2.5232@gmail.com> Co-authored-by: Alice "Arimah" Heurlin <30327355+arimah@users.noreply.github.com> Co-authored-by: MossyGreySlope <mossygreyslope@gmail.com> Co-authored-by: Gansu <68031780+GansuLalan@users.noreply.github.com> Co-authored-by: aa5g21 <aa5g21@soton.ac.uk> Co-authored-by: VideoKompany <135313844+VlaDOS1408@users.noreply.github.com> Co-authored-by: lunarcomets <140772713+lunarcomets@users.noreply.github.com> Co-authored-by: lunarcomets <luanrcomets2@gmail,com> Co-authored-by: compilatron <40789662+jbox144@users.noreply.github.com> Co-authored-by: Kill_Me_I_Noobs <118206719+Vonsant@users.noreply.github.com> Co-authored-by: Doctorjakes <122163951+Doctorjakes@users.noreply.github.com> Co-authored-by: Ko4ergaPunk <62609550+Ko4ergaPunk@users.noreply.github.com> Co-authored-by: Preston Smith <92108534+thetolbean@users.noreply.github.com> Co-authored-by: Token <56667933+TokenStyle@users.noreply.github.com> Co-authored-by: Nikolai Korolev <korolevns98@gmail.com> Co-authored-by: Bloodcanis <113198922+Bloodcanis@users.noreply.github.com> Co-authored-by: JIPDawg <51352440+JIPDawg@users.noreply.github.com> Co-authored-by: JIPDawg <JIPDawg93@gmail.com> Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com> Co-authored-by: Emisse <99158783+Emisse@users.noreply.github.com> Co-authored-by: Velcroboy <107660393+IamVelcroboy@users.noreply.github.com> Co-authored-by: Velcroboy <velcroboy333@hotmail.com> Co-authored-by: Luiz Costa <33888056+luizwritescode@users.noreply.github.com> Co-authored-by: CheddaCheez <cheddacheezy@gmail.com> Co-authored-by: Partmedia <kevinz5000@gmail.com> Co-authored-by: Tap <tapiocaphobe@gmail.com> Co-authored-by: Hreno <hrenor@gmail.com> Co-authored-by: keronshb <54602815+keronshb@users.noreply.github.com> Co-authored-by: Morb0 <14136326+Morb0@users.noreply.github.com> Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com> Co-authored-by: Litogin <139079015+Litogin@users.noreply.github.com> Co-authored-by: lapatison <100279397+lapatison@users.noreply.github.com> Co-authored-by: lapatison <lapatisonsocial@gmail.com> Co-authored-by: NotSoDamn <75203942+NotSoDana@users.noreply.github.com> Co-authored-by: cfif126 <94059374+cfif126@users.noreply.github.com> Co-authored-by: MureixloI <132683811+MureixloI@users.noreply.github.com> Co-authored-by: kvant8 <163752943+kvant8@users.noreply.github.com> Co-authored-by: kvant8 <ar4477023rr.@gmail.com> Co-authored-by: lastPechkin <mevlyutov1958@gmail.com> Co-authored-by: AwareFoxy <135021509+AwareFoxy@users.noreply.github.com>
1062 lines
42 KiB
C#
1062 lines
42 KiB
C#
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using Content.Server.Administration.Managers;
|
|
using Content.Server.Afk;
|
|
using Content.Server.Database;
|
|
using Content.Server.Discord;
|
|
using Content.Server.GameTicking;
|
|
using Content.Server.Players.RateLimiting;
|
|
using Content.Shared.Administration;
|
|
using Content.Shared.CCVar;
|
|
using Content.Shared.GameTicking;
|
|
using Content.Shared.Mind;
|
|
using Content.Shared.Players.RateLimiting;
|
|
using JetBrains.Annotations;
|
|
using Robust.Server.Player;
|
|
using Robust.Shared;
|
|
using Robust.Shared.Configuration;
|
|
using Robust.Shared.Enums;
|
|
using Robust.Shared.Network;
|
|
using Robust.Shared.Player;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Server.Administration.Systems
|
|
{
|
|
[UsedImplicitly]
|
|
public sealed partial class BwoinkSystem : SharedBwoinkSystem
|
|
{
|
|
private const string RateLimitKey = "AdminHelp";
|
|
|
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
|
[Dependency] private readonly IAdminManager _adminManager = default!;
|
|
[Dependency] private readonly IConfigurationManager _config = default!;
|
|
[Dependency] private readonly IGameTiming _timing = default!;
|
|
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
|
|
[Dependency] private readonly GameTicker _gameTicker = default!;
|
|
[Dependency] private readonly SharedMindSystem _minds = default!;
|
|
[Dependency] private readonly IAfkManager _afkManager = default!;
|
|
[Dependency] private readonly IServerDbManager _dbManager = default!;
|
|
[Dependency] private readonly PlayerRateLimitManager _rateLimit = default!;
|
|
|
|
[GeneratedRegex(@"^https://discord\.com/api/webhooks/(\d+)/((?!.*/).*)$")]
|
|
private static partial Regex DiscordRegex();
|
|
|
|
private string _webhookUrl = string.Empty;
|
|
private WebhookData? _webhookData;
|
|
|
|
private string _onCallUrl = string.Empty;
|
|
private WebhookData? _onCallData;
|
|
|
|
private ISawmill _sawmill = default!;
|
|
private readonly HttpClient _httpClient = new();
|
|
|
|
private string _footerIconUrl = string.Empty;
|
|
private string _avatarUrl = string.Empty;
|
|
private string _serverName = string.Empty;
|
|
|
|
private readonly Dictionary<NetUserId, DiscordRelayInteraction> _relayMessages = new();
|
|
|
|
private Dictionary<NetUserId, string> _oldMessageIds = new();
|
|
private readonly Dictionary<NetUserId, Queue<DiscordRelayedData>> _messageQueues = new();
|
|
private readonly HashSet<NetUserId> _processingChannels = new();
|
|
private readonly Dictionary<NetUserId, (TimeSpan Timestamp, bool Typing)> _typingUpdateTimestamps = new();
|
|
private string _overrideClientName = string.Empty;
|
|
|
|
// Max embed description length is 4096, according to https://discord.com/developers/docs/resources/channel#embed-object-embed-limits
|
|
// Keep small margin, just to be safe
|
|
private const ushort DescriptionMax = 4000;
|
|
|
|
// Maximum length a message can be before it is cut off
|
|
// Should be shorter than DescriptionMax
|
|
private const ushort MessageLengthCap = 3000;
|
|
|
|
// Text to be used to cut off messages that are too long. Should be shorter than MessageLengthCap
|
|
private const string TooLongText = "... **(too long)**";
|
|
|
|
private int _maxAdditionalChars;
|
|
private readonly Dictionary<NetUserId, DateTime> _activeConversations = new();
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
Subs.CVar(_config, CCVars.DiscordOnCallWebhook, OnCallChanged, true);
|
|
|
|
Subs.CVar(_config, CCVars.DiscordAHelpWebhook, OnWebhookChanged, true);
|
|
Subs.CVar(_config, CCVars.DiscordAHelpFooterIcon, OnFooterIconChanged, true);
|
|
Subs.CVar(_config, CCVars.DiscordAHelpAvatar, OnAvatarChanged, true);
|
|
Subs.CVar(_config, CVars.GameHostName, OnServerNameChanged, true);
|
|
Subs.CVar(_config, CCVars.AdminAhelpOverrideClientName, OnOverrideChanged, true);
|
|
_sawmill = IoCManager.Resolve<ILogManager>().GetSawmill("AHELP");
|
|
|
|
var defaultParams = new AHelpMessageParams(
|
|
string.Empty,
|
|
string.Empty,
|
|
true,
|
|
_gameTicker.RoundDuration().ToString("hh\\:mm\\:ss"),
|
|
_gameTicker.RunLevel,
|
|
playedSound: false
|
|
);
|
|
_maxAdditionalChars = GenerateAHelpMessage(defaultParams).Message.Length;
|
|
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
|
|
|
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameRunLevelChanged);
|
|
SubscribeNetworkEvent<BwoinkClientTypingUpdated>(OnClientTypingUpdated);
|
|
SubscribeLocalEvent<RoundRestartCleanupEvent>(_ => _activeConversations.Clear());
|
|
|
|
_rateLimit.Register(
|
|
RateLimitKey,
|
|
new RateLimitRegistration(CCVars.AhelpRateLimitPeriod,
|
|
CCVars.AhelpRateLimitCount,
|
|
PlayerRateLimitedAction)
|
|
);
|
|
}
|
|
|
|
private async void OnCallChanged(string url)
|
|
{
|
|
_onCallUrl = url;
|
|
|
|
if (url == string.Empty)
|
|
return;
|
|
|
|
var match = DiscordRegex().Match(url);
|
|
|
|
if (!match.Success)
|
|
{
|
|
Log.Error("On call URL does not appear to be valid.");
|
|
return;
|
|
}
|
|
|
|
if (match.Groups.Count <= 2)
|
|
{
|
|
Log.Error("Could not get webhook ID or token for on call URL.");
|
|
return;
|
|
}
|
|
|
|
var webhookId = match.Groups[1].Value;
|
|
var webhookToken = match.Groups[2].Value;
|
|
|
|
_onCallData = await GetWebhookData(webhookId, webhookToken);
|
|
}
|
|
|
|
private void PlayerRateLimitedAction(ICommonSession obj)
|
|
{
|
|
RaiseNetworkEvent(
|
|
new BwoinkTextMessage(obj.UserId, default, Loc.GetString("bwoink-system-rate-limited"), playSound: false),
|
|
obj.Channel);
|
|
}
|
|
|
|
private void OnOverrideChanged(string obj)
|
|
{
|
|
_overrideClientName = obj;
|
|
}
|
|
|
|
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
|
{
|
|
if (e.NewStatus == SessionStatus.Disconnected)
|
|
{
|
|
if (_activeConversations.TryGetValue(e.Session.UserId, out var lastMessageTime))
|
|
{
|
|
var timeSinceLastMessage = DateTime.Now - lastMessageTime;
|
|
if (timeSinceLastMessage > TimeSpan.FromMinutes(5))
|
|
{
|
|
_activeConversations.Remove(e.Session.UserId);
|
|
return; // Do not send disconnect message if timeout exceeded
|
|
}
|
|
}
|
|
|
|
// Check if the user has been banned
|
|
var ban = await _dbManager.GetServerBanAsync(null, e.Session.UserId, null, null);
|
|
if (ban != null)
|
|
{
|
|
var banMessage = Loc.GetString("bwoink-system-player-banned", ("banReason", ban.Reason));
|
|
NotifyAdmins(e.Session, banMessage, PlayerStatusType.Banned);
|
|
_activeConversations.Remove(e.Session.UserId);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Notify all admins if a player disconnects or reconnects
|
|
var message = e.NewStatus switch
|
|
{
|
|
SessionStatus.Connected => Loc.GetString("bwoink-system-player-reconnecting"),
|
|
SessionStatus.Disconnected => Loc.GetString("bwoink-system-player-disconnecting"),
|
|
_ => null
|
|
};
|
|
|
|
if (message != null)
|
|
{
|
|
var statusType = e.NewStatus == SessionStatus.Connected
|
|
? PlayerStatusType.Connected
|
|
: PlayerStatusType.Disconnected;
|
|
NotifyAdmins(e.Session, message, statusType);
|
|
}
|
|
|
|
if (e.NewStatus != SessionStatus.InGame)
|
|
return;
|
|
|
|
RaiseNetworkEvent(new BwoinkDiscordRelayUpdated(!string.IsNullOrWhiteSpace(_webhookUrl)), e.Session);
|
|
}
|
|
|
|
private void NotifyAdmins(ICommonSession session, string message, PlayerStatusType statusType)
|
|
{
|
|
if (!_activeConversations.ContainsKey(session.UserId))
|
|
{
|
|
// If the user is not part of an active conversation, do not notify admins.
|
|
return;
|
|
}
|
|
|
|
// Get the current timestamp
|
|
var timestamp = DateTime.Now.ToString("HH:mm:ss");
|
|
var roundTime = _gameTicker.RoundDuration().ToString("hh\\:mm\\:ss");
|
|
|
|
// Determine the icon based on the status type
|
|
string icon = statusType switch
|
|
{
|
|
PlayerStatusType.Connected => ":green_circle:",
|
|
PlayerStatusType.Disconnected => ":red_circle:",
|
|
PlayerStatusType.Banned => ":no_entry:",
|
|
_ => ":question:"
|
|
};
|
|
|
|
// Create the message parameters for Discord
|
|
var messageParams = new AHelpMessageParams(
|
|
session.Name,
|
|
message,
|
|
true,
|
|
roundTime,
|
|
_gameTicker.RunLevel,
|
|
playedSound: true,
|
|
icon: icon
|
|
);
|
|
|
|
// Create the message for in-game with username
|
|
var color = statusType switch
|
|
{
|
|
PlayerStatusType.Connected => Color.Green.ToHex(),
|
|
PlayerStatusType.Disconnected => Color.Yellow.ToHex(),
|
|
PlayerStatusType.Banned => Color.Orange.ToHex(),
|
|
_ => Color.Gray.ToHex(),
|
|
};
|
|
var inGameMessage = $"[color={color}]{session.Name} {message}[/color]";
|
|
|
|
var bwoinkMessage = new BwoinkTextMessage(
|
|
userId: session.UserId,
|
|
trueSender: SystemUserId,
|
|
text: inGameMessage,
|
|
sentAt: DateTime.Now,
|
|
playSound: false
|
|
);
|
|
|
|
var admins = GetTargetAdmins();
|
|
foreach (var admin in admins)
|
|
{
|
|
RaiseNetworkEvent(bwoinkMessage, admin);
|
|
}
|
|
|
|
// Enqueue the message for Discord relay
|
|
if (_webhookUrl != string.Empty)
|
|
{
|
|
// if (!_messageQueues.ContainsKey(session.UserId))
|
|
// _messageQueues[session.UserId] = new Queue<string>();
|
|
//
|
|
// var escapedText = FormattedMessage.EscapeText(message);
|
|
// messageParams.Message = escapedText;
|
|
//
|
|
// var discordMessage = GenerateAHelpMessage(messageParams);
|
|
// _messageQueues[session.UserId].Enqueue(discordMessage);
|
|
|
|
var queue = _messageQueues.GetOrNew(session.UserId);
|
|
var escapedText = FormattedMessage.EscapeText(message);
|
|
messageParams.Message = escapedText;
|
|
var discordMessage = GenerateAHelpMessage(messageParams);
|
|
queue.Enqueue(discordMessage);
|
|
}
|
|
}
|
|
|
|
private void OnGameRunLevelChanged(GameRunLevelChangedEvent args)
|
|
{
|
|
// Don't make a new embed if we
|
|
// 1. were in the lobby just now, and
|
|
// 2. are not entering the lobby or directly into a new round.
|
|
if (args.Old is GameRunLevel.PreRoundLobby ||
|
|
args.New is not (GameRunLevel.PreRoundLobby or GameRunLevel.InRound))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Store the Discord message IDs of the previous round
|
|
_oldMessageIds = new Dictionary<NetUserId, string>();
|
|
foreach (var (user, interaction) in _relayMessages)
|
|
{
|
|
var id = interaction.Id;
|
|
if (id == null)
|
|
return;
|
|
|
|
_oldMessageIds[user] = id;
|
|
}
|
|
|
|
_relayMessages.Clear();
|
|
}
|
|
|
|
private void OnClientTypingUpdated(BwoinkClientTypingUpdated msg, EntitySessionEventArgs args)
|
|
{
|
|
if (_typingUpdateTimestamps.TryGetValue(args.SenderSession.UserId, out var tuple) &&
|
|
tuple.Typing == msg.Typing &&
|
|
tuple.Timestamp + TimeSpan.FromSeconds(1) > _timing.RealTime)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_typingUpdateTimestamps[args.SenderSession.UserId] = (_timing.RealTime, msg.Typing);
|
|
|
|
// Non-admins can only ever type on their own ahelp, guard against fake messages
|
|
var isAdmin = _adminManager.GetAdminData(args.SenderSession)?.HasFlag(AdminFlags.Adminhelp) ?? false;
|
|
var channel = isAdmin ? msg.Channel : args.SenderSession.UserId;
|
|
var update = new BwoinkPlayerTypingUpdated(channel, args.SenderSession.Name, msg.Typing);
|
|
|
|
foreach (var admin in GetTargetAdmins())
|
|
{
|
|
if (admin.UserId == args.SenderSession.UserId)
|
|
continue;
|
|
|
|
RaiseNetworkEvent(update, admin);
|
|
}
|
|
}
|
|
|
|
private void OnServerNameChanged(string obj)
|
|
{
|
|
_serverName = obj;
|
|
}
|
|
|
|
private async void OnWebhookChanged(string url)
|
|
{
|
|
_webhookUrl = url;
|
|
|
|
RaiseNetworkEvent(new BwoinkDiscordRelayUpdated(!string.IsNullOrWhiteSpace(url)));
|
|
|
|
if (url == string.Empty)
|
|
return;
|
|
|
|
// Basic sanity check and capturing webhook ID and token
|
|
var match = DiscordRegex().Match(url);
|
|
|
|
if (!match.Success)
|
|
{
|
|
// TODO: Ideally, CVar validation during setting should be better integrated
|
|
Log.Warning("Webhook URL does not appear to be valid. Using anyways...");
|
|
return;
|
|
}
|
|
|
|
if (match.Groups.Count <= 2)
|
|
{
|
|
Log.Error("Could not get webhook ID or token.");
|
|
return;
|
|
}
|
|
|
|
var webhookId = match.Groups[1].Value;
|
|
var webhookToken = match.Groups[2].Value;
|
|
|
|
// Fire and forget
|
|
_webhookData = await GetWebhookData(webhookId, webhookToken);
|
|
}
|
|
|
|
private async Task<WebhookData?> GetWebhookData(string id, string token)
|
|
{
|
|
var response = await _httpClient.GetAsync($"https://discord.com/api/v10/webhooks/{id}/{token}");
|
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
_sawmill.Log(LogLevel.Error,
|
|
$"Discord returned bad status code when trying to get webhook data (perhaps the webhook URL is invalid?): {response.StatusCode}\nResponse: {content}");
|
|
return null;
|
|
}
|
|
|
|
return JsonSerializer.Deserialize<WebhookData>(content);
|
|
}
|
|
|
|
private void OnFooterIconChanged(string url)
|
|
{
|
|
_footerIconUrl = url;
|
|
}
|
|
|
|
private void OnAvatarChanged(string url)
|
|
{
|
|
_avatarUrl = url;
|
|
}
|
|
|
|
private async void ProcessQueue(NetUserId userId, Queue<DiscordRelayedData> messages)
|
|
{
|
|
// Whether an embed already exists for this player
|
|
var exists = _relayMessages.TryGetValue(userId, out var existingEmbed);
|
|
|
|
// Whether the message will become too long after adding these new messages
|
|
var tooLong = exists && messages.Sum(msg => Math.Min(msg.Message.Length, MessageLengthCap) + "\n".Length)
|
|
+ existingEmbed?.Description.Length > DescriptionMax;
|
|
|
|
// If there is no existing embed, or it is getting too long, we create a new embed
|
|
if (!exists || tooLong)
|
|
{
|
|
var lookup = await _playerLocator.LookupIdAsync(userId);
|
|
|
|
if (lookup == null)
|
|
{
|
|
_sawmill.Log(LogLevel.Error,
|
|
$"Unable to find player for NetUserId {userId} when sending discord webhook.");
|
|
_relayMessages.Remove(userId);
|
|
return;
|
|
}
|
|
|
|
var linkToPrevious = string.Empty;
|
|
|
|
// If we have all the data required, we can link to the embed of the previous round or embed that was too long
|
|
if (_webhookData is { GuildId: { } guildId, ChannelId: { } channelId })
|
|
{
|
|
if (tooLong && existingEmbed?.Id != null)
|
|
{
|
|
linkToPrevious =
|
|
$"**[Go to previous embed of this round](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.Id})**\n";
|
|
}
|
|
else if (_oldMessageIds.TryGetValue(userId, out var id) && !string.IsNullOrEmpty(id))
|
|
{
|
|
linkToPrevious =
|
|
$"**[Go to last round's conversation with this player](https://discord.com/channels/{guildId}/{channelId}/{id})**\n";
|
|
}
|
|
}
|
|
|
|
var characterName = _minds.GetCharacterName(userId);
|
|
existingEmbed = new DiscordRelayInteraction()
|
|
{
|
|
Id = null,
|
|
CharacterName = characterName,
|
|
Description = linkToPrevious,
|
|
Username = lookup.Username,
|
|
LastRunLevel = _gameTicker.RunLevel,
|
|
};
|
|
|
|
_relayMessages[userId] = existingEmbed;
|
|
}
|
|
|
|
// Previous message was in another RunLevel, so show that in the embed
|
|
if (existingEmbed!.LastRunLevel != _gameTicker.RunLevel)
|
|
{
|
|
existingEmbed.Description += _gameTicker.RunLevel switch
|
|
{
|
|
GameRunLevel.PreRoundLobby => "\n\n:arrow_forward: _**Pre-round lobby started**_\n",
|
|
GameRunLevel.InRound => "\n\n:arrow_forward: _**Round started**_\n",
|
|
GameRunLevel.PostRound => "\n\n:stop_button: _**Post-round started**_\n",
|
|
_ => throw new ArgumentOutOfRangeException(nameof(_gameTicker.RunLevel),
|
|
$"{_gameTicker.RunLevel} was not matched."),
|
|
};
|
|
|
|
existingEmbed.LastRunLevel = _gameTicker.RunLevel;
|
|
}
|
|
|
|
// If last message of the new batch is SOS then relay it to on-call.
|
|
// ... as long as it hasn't been relayed already.
|
|
var discordMention = messages.Last();
|
|
var onCallRelay = !discordMention.Receivers && !existingEmbed.OnCall;
|
|
|
|
// Add available messages to the embed description
|
|
while (messages.TryDequeue(out var message))
|
|
{
|
|
string text;
|
|
|
|
// In case someone thinks they're funny
|
|
if (message.Message.Length > MessageLengthCap)
|
|
text = message.Message[..(MessageLengthCap - TooLongText.Length)] + TooLongText;
|
|
else
|
|
text = message.Message;
|
|
|
|
existingEmbed.Description += $"\n{text}";
|
|
}
|
|
|
|
var payload = GeneratePayload(existingEmbed.Description,
|
|
existingEmbed.Username,
|
|
existingEmbed.CharacterName);
|
|
|
|
// If there is no existing embed, create a new one
|
|
// Otherwise patch (edit) it
|
|
if (existingEmbed.Id == null)
|
|
{
|
|
var request = await _httpClient.PostAsync($"{_webhookUrl}?wait=true",
|
|
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
|
|
|
|
var content = await request.Content.ReadAsStringAsync();
|
|
if (!request.IsSuccessStatusCode)
|
|
{
|
|
_sawmill.Log(LogLevel.Error,
|
|
$"Discord returned bad status code when posting message (perhaps the message is too long?): {request.StatusCode}\nResponse: {content}");
|
|
_relayMessages.Remove(userId);
|
|
return;
|
|
}
|
|
|
|
var id = JsonNode.Parse(content)?["id"];
|
|
if (id == null)
|
|
{
|
|
_sawmill.Log(LogLevel.Error,
|
|
$"Could not find id in json-content returned from discord webhook: {content}");
|
|
_relayMessages.Remove(userId);
|
|
return;
|
|
}
|
|
|
|
existingEmbed.Id = id.ToString();
|
|
}
|
|
else
|
|
{
|
|
var request = await _httpClient.PatchAsync($"{_webhookUrl}/messages/{existingEmbed.Id}",
|
|
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
|
|
|
|
if (!request.IsSuccessStatusCode)
|
|
{
|
|
var content = await request.Content.ReadAsStringAsync();
|
|
_sawmill.Log(LogLevel.Error,
|
|
$"Discord returned bad status code when patching message (perhaps the message is too long?): {request.StatusCode}\nResponse: {content}");
|
|
_relayMessages.Remove(userId);
|
|
return;
|
|
}
|
|
}
|
|
|
|
_relayMessages[userId] = existingEmbed;
|
|
|
|
// Actually do the on call relay last, we just need to grab it before we dequeue every message above.
|
|
if (onCallRelay &&
|
|
_onCallData != null)
|
|
{
|
|
existingEmbed.OnCall = true;
|
|
var roleMention = _config.GetCVar(CCVars.DiscordAhelpMention);
|
|
|
|
if (!string.IsNullOrEmpty(roleMention))
|
|
{
|
|
var message = new StringBuilder();
|
|
message.AppendLine($"<@&{roleMention}>");
|
|
message.AppendLine("Unanswered SOS");
|
|
|
|
// Need webhook data to get the correct link for that channel rather than on-call data.
|
|
if (_webhookData is { GuildId: { } guildId, ChannelId: { } channelId })
|
|
{
|
|
message.AppendLine(
|
|
$"**[Go to ahelp](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.Id})**");
|
|
}
|
|
|
|
payload = GeneratePayload(message.ToString(), existingEmbed.Username, existingEmbed.CharacterName);
|
|
|
|
var request = await _httpClient.PostAsync($"{_onCallUrl}?wait=true",
|
|
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
|
|
|
|
var content = await request.Content.ReadAsStringAsync();
|
|
if (!request.IsSuccessStatusCode)
|
|
{
|
|
_sawmill.Log(LogLevel.Error, $"Discord returned bad status code when posting relay message (perhaps the message is too long?): {request.StatusCode}\nResponse: {content}");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
existingEmbed.OnCall = false;
|
|
}
|
|
|
|
_processingChannels.Remove(userId);
|
|
}
|
|
|
|
private WebhookPayload GeneratePayload(string messages, string username, string? characterName = null)
|
|
{
|
|
// Add character name
|
|
if (characterName != null)
|
|
username += $" ({characterName})";
|
|
|
|
// If no admins are online, set embed color to red. Otherwise green
|
|
var color = GetNonAfkAdmins().Count > 0 ? 0x41F097 : 0xFF0000;
|
|
|
|
// Limit server name to 1500 characters, in case someone tries to be a little funny
|
|
var serverName = _serverName[..Math.Min(_serverName.Length, 1500)];
|
|
|
|
var round = _gameTicker.RunLevel switch
|
|
{
|
|
GameRunLevel.PreRoundLobby => _gameTicker.RoundId == 0
|
|
? "pre-round lobby after server restart" // first round after server restart has ID == 0
|
|
: $"pre-round lobby for round {_gameTicker.RoundId + 1}",
|
|
GameRunLevel.InRound => $"round {_gameTicker.RoundId}",
|
|
GameRunLevel.PostRound => $"post-round {_gameTicker.RoundId}",
|
|
_ => throw new ArgumentOutOfRangeException(nameof(_gameTicker.RunLevel),
|
|
$"{_gameTicker.RunLevel} was not matched."),
|
|
};
|
|
|
|
return new WebhookPayload
|
|
{
|
|
Username = username,
|
|
AvatarUrl = string.IsNullOrWhiteSpace(_avatarUrl) ? null : _avatarUrl,
|
|
Embeds = new List<WebhookEmbed>
|
|
{
|
|
new()
|
|
{
|
|
Description = messages,
|
|
Color = color,
|
|
Footer = new WebhookEmbedFooter
|
|
{
|
|
Text = $"{serverName} ({round})",
|
|
IconUrl = string.IsNullOrWhiteSpace(_footerIconUrl) ? null : _footerIconUrl
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
base.Update(frameTime);
|
|
|
|
foreach (var userId in _messageQueues.Keys.ToArray())
|
|
{
|
|
if (_processingChannels.Contains(userId))
|
|
continue;
|
|
|
|
var queue = _messageQueues[userId];
|
|
_messageQueues.Remove(userId);
|
|
if (queue.Count == 0)
|
|
continue;
|
|
|
|
_processingChannels.Add(userId);
|
|
|
|
ProcessQueue(userId, queue);
|
|
}
|
|
}
|
|
|
|
protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySessionEventArgs eventArgs)
|
|
{
|
|
base.OnBwoinkTextMessage(message, eventArgs);
|
|
_activeConversations[message.UserId] = DateTime.Now;
|
|
var senderSession = eventArgs.SenderSession;
|
|
|
|
// TODO: Sanitize text?
|
|
// Confirm that this person is actually allowed to send a message here.
|
|
var personalChannel = senderSession.UserId == message.UserId;
|
|
var senderAdmin = _adminManager.GetAdminData(senderSession);
|
|
var senderAHelpAdmin = senderAdmin?.HasFlag(AdminFlags.Adminhelp) ?? false;
|
|
var authorized = personalChannel || senderAHelpAdmin;
|
|
if (!authorized)
|
|
{
|
|
// Unauthorized bwoink (log?)
|
|
return;
|
|
}
|
|
|
|
if (_rateLimit.CountAction(eventArgs.SenderSession, RateLimitKey) != RateLimitStatus.Allowed)
|
|
return;
|
|
|
|
var escapedText = FormattedMessage.EscapeText(message.Text);
|
|
|
|
string bwoinkText;
|
|
string adminPrefix = "";
|
|
|
|
//Getting an administrator position
|
|
if (_config.GetCVar(CCVars.AhelpAdminPrefix) && senderAdmin is not null && senderAdmin.Title is not null)
|
|
{
|
|
adminPrefix = $"[bold]\\[{senderAdmin.Title}\\][/bold] ";
|
|
}
|
|
|
|
if (senderAdmin is not null &&
|
|
senderAdmin.Flags ==
|
|
AdminFlags.Adminhelp) // Mentor. Not full admin. That's why it's colored differently.
|
|
{
|
|
bwoinkText = $"[color=purple]{adminPrefix}{senderSession.Name}[/color]";
|
|
}
|
|
else if (senderAdmin is not null && senderAdmin.HasFlag(AdminFlags.Adminhelp))
|
|
{
|
|
bwoinkText = $"[color=red]{adminPrefix}{senderSession.Name}[/color]";
|
|
}
|
|
else
|
|
{
|
|
bwoinkText = $"{senderSession.Name}";
|
|
}
|
|
|
|
bwoinkText = $"{(message.PlaySound ? "" : "(S) ")}{bwoinkText}: {escapedText}";
|
|
|
|
// If it's not an admin / admin chooses to keep the sound then play it.
|
|
var playSound = !senderAHelpAdmin || message.PlaySound;
|
|
var msg = new BwoinkTextMessage(message.UserId, senderSession.UserId, bwoinkText, playSound: playSound);
|
|
|
|
LogBwoink(msg);
|
|
|
|
var admins = GetTargetAdmins();
|
|
|
|
// Notify all admins
|
|
foreach (var channel in admins)
|
|
{
|
|
RaiseNetworkEvent(msg, channel);
|
|
}
|
|
|
|
string adminPrefixWebhook = "";
|
|
|
|
if (_config.GetCVar(CCVars.AhelpAdminPrefixWebhook) && senderAdmin is not null && senderAdmin.Title is not null)
|
|
{
|
|
adminPrefixWebhook = $"[bold]\\[{senderAdmin.Title}\\][/bold] ";
|
|
}
|
|
|
|
// Notify player
|
|
if (_playerManager.TryGetSessionById(message.UserId, out var session))
|
|
{
|
|
if (!admins.Contains(session.Channel))
|
|
{
|
|
// If _overrideClientName is set, we generate a new message with the override name. The admins name will still be the original name for the webhooks.
|
|
if (_overrideClientName != string.Empty)
|
|
{
|
|
string overrideMsgText;
|
|
// Doing the same thing as above, but with the override name. Theres probably a better way to do this.
|
|
if (senderAdmin is not null &&
|
|
senderAdmin.Flags ==
|
|
AdminFlags.Adminhelp) // Mentor. Not full admin. That's why it's colored differently.
|
|
{
|
|
overrideMsgText = $"[color=purple]{adminPrefixWebhook}{_overrideClientName}[/color]";
|
|
}
|
|
else if (senderAdmin is not null && senderAdmin.HasFlag(AdminFlags.Adminhelp))
|
|
{
|
|
overrideMsgText = $"[color=red]{adminPrefixWebhook}{_overrideClientName}[/color]";
|
|
}
|
|
else
|
|
{
|
|
overrideMsgText = $"{senderSession.Name}"; // Not an admin, name is not overridden.
|
|
}
|
|
|
|
overrideMsgText = $"{(message.PlaySound ? "" : "(S) ")}{overrideMsgText}: {escapedText}";
|
|
|
|
RaiseNetworkEvent(new BwoinkTextMessage(message.UserId,
|
|
senderSession.UserId,
|
|
overrideMsgText,
|
|
playSound: playSound),
|
|
session.Channel);
|
|
}
|
|
else
|
|
RaiseNetworkEvent(msg, session.Channel);
|
|
}
|
|
}
|
|
|
|
var sendsWebhook = _webhookUrl != string.Empty;
|
|
if (sendsWebhook)
|
|
{
|
|
if (!_messageQueues.ContainsKey(msg.UserId))
|
|
_messageQueues[msg.UserId] = new Queue<DiscordRelayedData>();
|
|
|
|
var str = message.Text;
|
|
var unameLength = senderSession.Name.Length;
|
|
|
|
if (unameLength + str.Length + _maxAdditionalChars > DescriptionMax)
|
|
{
|
|
str = str[..(DescriptionMax - _maxAdditionalChars - unameLength)];
|
|
}
|
|
|
|
var nonAfkAdmins = GetNonAfkAdmins();
|
|
var messageParams = new AHelpMessageParams(
|
|
senderSession.Name,
|
|
str,
|
|
!personalChannel,
|
|
_gameTicker.RoundDuration().ToString("hh\\:mm\\:ss"),
|
|
_gameTicker.RunLevel,
|
|
playedSound: playSound,
|
|
noReceivers: nonAfkAdmins.Count == 0
|
|
);
|
|
_messageQueues[msg.UserId].Enqueue(GenerateAHelpMessage(messageParams));
|
|
}
|
|
|
|
if (admins.Count != 0 || sendsWebhook)
|
|
return;
|
|
|
|
// No admin online, let the player know
|
|
var systemText = Loc.GetString("bwoink-system-starmute-message-no-other-users");
|
|
var starMuteMsg = new BwoinkTextMessage(message.UserId, SystemUserId, systemText);
|
|
RaiseNetworkEvent(starMuteMsg, senderSession.Channel);
|
|
}
|
|
|
|
private IList<INetChannel> GetNonAfkAdmins()
|
|
{
|
|
return _adminManager.ActiveAdmins
|
|
.Where(p => (_adminManager.GetAdminData(p)?.HasFlag(AdminFlags.Adminhelp) ?? false) &&
|
|
!_afkManager.IsAfk(p))
|
|
.Select(p => p.Channel)
|
|
.ToList();
|
|
}
|
|
|
|
private IList<INetChannel> GetTargetAdmins()
|
|
{
|
|
return _adminManager.ActiveAdmins
|
|
.Where(p => _adminManager.GetAdminData(p)?.HasFlag(AdminFlags.Adminhelp) ?? false)
|
|
.Select(p => p.Channel)
|
|
.ToList();
|
|
}
|
|
|
|
//WL-Changes-start
|
|
/// <summary>
|
|
/// КОПИЯ МЕТОДА <see cref="OnBwoinkTextMessage(BwoinkTextMessage, EntitySessionEventArgs)">
|
|
/// </summary>
|
|
/// <param name="message"></param>
|
|
/// <param name="senderName"></param>
|
|
/// <param name="senderNetId"></param>
|
|
public async Task<bool> HandleDiscordAhelp(BwoinkTextMessage message, string senderName, NetUserId senderNetId, bool check_admin)
|
|
{
|
|
_activeConversations[message.UserId] = DateTime.Now;
|
|
|
|
// TODO: Sanitize text?
|
|
// Confirm that this person is actually allowed to send a message here.
|
|
var personalChannel = senderNetId == message.UserId;
|
|
|
|
var senderAdminDb = (Admin?)null;
|
|
if (check_admin)
|
|
{
|
|
senderAdminDb = await _dbManager.GetAdminDataForAsync(senderNetId);
|
|
if (senderAdminDb == null)
|
|
return false;
|
|
}
|
|
|
|
var adminFlags = AdminFlagsHelper.NamesToFlags(senderAdminDb?.AdminRank?.Flags.Select(f => f.Flag) ?? []);
|
|
|
|
var adminData = new AdminData()
|
|
{
|
|
Active = true,
|
|
Stealth = false,
|
|
Title = senderAdminDb?.Title ?? "STUFF",
|
|
Flags = adminFlags
|
|
};
|
|
|
|
var senderAHelpAdmin = adminData.HasFlag(AdminFlags.Adminhelp);
|
|
var authorized = personalChannel || senderAHelpAdmin;
|
|
if (!authorized)
|
|
{
|
|
// Unauthorized bwoink (log?)
|
|
return false;
|
|
}
|
|
|
|
var escapedText = FormattedMessage.EscapeText(message.Text);
|
|
|
|
string bwoinkText;
|
|
string adminPrefix = "";
|
|
|
|
//Getting an administrator position
|
|
if (_config.GetCVar(CCVars.AhelpAdminPrefix) && adminData is not null && adminData.Title is not null)
|
|
{
|
|
adminPrefix = $"[bold]\\[{adminData.Title}\\][/bold] ";
|
|
}
|
|
|
|
if (adminData is not null &&
|
|
adminFlags ==
|
|
AdminFlags.Adminhelp) // Mentor. Not full admin. That's why it's colored differently.
|
|
{
|
|
bwoinkText = $"[color=purple]{adminPrefix}{senderName}[/color]";
|
|
}
|
|
else if (adminData is not null && adminData.HasFlag(AdminFlags.Adminhelp))
|
|
{
|
|
bwoinkText = $"[color=red]{adminPrefix}{senderName}[/color]";
|
|
}
|
|
else
|
|
{
|
|
bwoinkText = $"{senderName}";
|
|
}
|
|
|
|
bwoinkText = $"{(message.PlaySound ? "" : "(S) ")}{bwoinkText}: {escapedText}";
|
|
|
|
// If it's not an admin / admin chooses to keep the sound then play it.
|
|
var playSound = !senderAHelpAdmin || message.PlaySound;
|
|
var msg = new BwoinkTextMessage(message.UserId, senderNetId, bwoinkText, playSound: playSound);
|
|
|
|
LogBwoink(msg);
|
|
|
|
var admins = GetTargetAdmins();
|
|
|
|
// Notify all admins
|
|
foreach (var channel in admins)
|
|
{
|
|
RaiseNetworkEvent(msg, channel);
|
|
}
|
|
|
|
string adminPrefixWebhook = "";
|
|
|
|
if (_config.GetCVar(CCVars.AhelpAdminPrefixWebhook) && adminData is not null && adminData.Title is not null)
|
|
{
|
|
adminPrefixWebhook = $"[bold]\\[{adminData.Title}\\][/bold] ";
|
|
}
|
|
|
|
// Notify player
|
|
if (_playerManager.TryGetSessionById(message.UserId, out var session))
|
|
{
|
|
if (!admins.Contains(session.Channel))
|
|
{
|
|
// If _overrideClientName is set, we generate a new message with the override name. The admins name will still be the original name for the webhooks.
|
|
if (_overrideClientName != string.Empty)
|
|
{
|
|
string overrideMsgText;
|
|
// Doing the same thing as above, but with the override name. Theres probably a better way to do this.
|
|
if (adminData is not null &&
|
|
adminFlags ==
|
|
AdminFlags.Adminhelp) // Mentor. Not full admin. That's why it's colored differently.
|
|
{
|
|
overrideMsgText = $"[color=purple]{adminPrefixWebhook}{_overrideClientName}[/color]";
|
|
}
|
|
else if (adminData is not null && adminData.HasFlag(AdminFlags.Adminhelp))
|
|
{
|
|
overrideMsgText = $"[color=red]{adminPrefixWebhook}{_overrideClientName}[/color]";
|
|
}
|
|
else
|
|
{
|
|
overrideMsgText = $"{senderName}"; // Not an admin, name is not overridden.
|
|
}
|
|
|
|
overrideMsgText = $"{(message.PlaySound ? "" : "(S) ")}{overrideMsgText}: {escapedText}";
|
|
|
|
RaiseNetworkEvent(new BwoinkTextMessage(message.UserId,
|
|
senderNetId,
|
|
overrideMsgText,
|
|
playSound: playSound),
|
|
session.Channel);
|
|
}
|
|
else
|
|
RaiseNetworkEvent(msg, session.Channel);
|
|
}
|
|
}
|
|
|
|
var sendsWebhook = _webhookUrl != string.Empty;
|
|
if (sendsWebhook)
|
|
{
|
|
if (!_messageQueues.ContainsKey(msg.UserId))
|
|
_messageQueues[msg.UserId] = new Queue<DiscordRelayedData>();
|
|
|
|
var str = message.Text;
|
|
var unameLength = senderName.Length;
|
|
|
|
if (unameLength + str.Length + _maxAdditionalChars > DescriptionMax)
|
|
{
|
|
str = str[..(DescriptionMax - _maxAdditionalChars - unameLength)];
|
|
}
|
|
|
|
var nonAfkAdmins = GetNonAfkAdmins();
|
|
var messageParams = new AHelpMessageParams(
|
|
senderName,
|
|
str,
|
|
!personalChannel,
|
|
_gameTicker.RoundDuration().ToString("hh\\:mm\\:ss"),
|
|
_gameTicker.RunLevel,
|
|
playedSound: playSound,
|
|
noReceivers: nonAfkAdmins.Count == 0
|
|
);
|
|
_messageQueues[msg.UserId].Enqueue(GenerateAHelpMessage(messageParams));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
//WL-Changes-end
|
|
|
|
private static DiscordRelayedData GenerateAHelpMessage(AHelpMessageParams parameters)
|
|
{
|
|
var stringbuilder = new StringBuilder();
|
|
|
|
if (parameters.Icon != null)
|
|
stringbuilder.Append(parameters.Icon);
|
|
else if (parameters.IsAdmin)
|
|
stringbuilder.Append(":outbox_tray:");
|
|
else if (parameters.NoReceivers)
|
|
stringbuilder.Append(":sos:");
|
|
else
|
|
stringbuilder.Append(":inbox_tray:");
|
|
|
|
if (parameters.RoundTime != string.Empty && parameters.RoundState == GameRunLevel.InRound)
|
|
stringbuilder.Append($" **{parameters.RoundTime}**");
|
|
if (!parameters.PlayedSound)
|
|
stringbuilder.Append(" **(S)**");
|
|
if (parameters.Icon == null)
|
|
stringbuilder.Append($" **{parameters.Username}:** ");
|
|
else
|
|
stringbuilder.Append($" **{parameters.Username}** ");
|
|
stringbuilder.Append(parameters.Message);
|
|
|
|
return new DiscordRelayedData()
|
|
{
|
|
Receivers = !parameters.NoReceivers,
|
|
Message = stringbuilder.ToString(),
|
|
};
|
|
}
|
|
|
|
private record struct DiscordRelayedData
|
|
{
|
|
/// <summary>
|
|
/// Was anyone online to receive it.
|
|
/// </summary>
|
|
public bool Receivers;
|
|
|
|
/// <summary>
|
|
/// What's the payload to send to discord.
|
|
/// </summary>
|
|
public string Message;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Class specifically for holding information regarding existing Discord embeds
|
|
/// </summary>
|
|
private sealed class DiscordRelayInteraction
|
|
{
|
|
public string? Id;
|
|
|
|
public string Username = String.Empty;
|
|
|
|
public string? CharacterName;
|
|
|
|
/// <summary>
|
|
/// Contents for the discord message.
|
|
/// </summary>
|
|
public string Description = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Run level of the last interaction. If different we'll link to the last Id.
|
|
/// </summary>
|
|
public GameRunLevel LastRunLevel;
|
|
|
|
/// <summary>
|
|
/// Did we relay this interaction to OnCall previously.
|
|
/// </summary>
|
|
public bool OnCall;
|
|
}
|
|
}
|
|
|
|
public sealed class AHelpMessageParams
|
|
{
|
|
public string Username { get; set; }
|
|
public string Message { get; set; }
|
|
public bool IsAdmin { get; set; }
|
|
public string RoundTime { get; set; }
|
|
public GameRunLevel RoundState { get; set; }
|
|
public bool PlayedSound { get; set; }
|
|
public bool NoReceivers { get; set; }
|
|
public string? Icon { get; set; }
|
|
|
|
public AHelpMessageParams(
|
|
string username,
|
|
string message,
|
|
bool isAdmin,
|
|
string roundTime,
|
|
GameRunLevel roundState,
|
|
bool playedSound,
|
|
bool noReceivers = false,
|
|
string? icon = null)
|
|
{
|
|
Username = username;
|
|
Message = message;
|
|
IsAdmin = isAdmin;
|
|
RoundTime = roundTime;
|
|
RoundState = roundState;
|
|
PlayedSound = playedSound;
|
|
NoReceivers = noReceivers;
|
|
Icon = icon;
|
|
}
|
|
}
|
|
|
|
public enum PlayerStatusType
|
|
{
|
|
Connected,
|
|
Disconnected,
|
|
Banned,
|
|
}
|
|
}
|