Compare commits

...

196 Commits

Author SHA1 Message Date
MSWS
9b1bed6982 Begin work on tripwire item 2025-11-03 16:53:39 -08:00
MSWS
8584877739 feat: Replicate death events to fellow traitors (resolves #171) +semver:minor 2025-11-03 15:30:44 -08:00
MSWS
410dd407b3 update: Handle tag colors as well 2025-11-02 21:23:19 -08:00
MSWS
a0bba2c4ba Add warden tag 2025-11-02 21:16:40 -08:00
MSWS
8aa508bf6d feat: Add victim message to 1-hit weapon 2025-11-02 20:45:23 -08:00
MSWS
642155b1bc Revert "fix: Add check after roles assigned"
This reverts commit bacd288fe7.
2025-11-02 20:41:01 -08:00
MSWS
bacd288fe7 fix: Add check after roles assigned 2025-11-02 20:40:05 -08:00
MSWS
29e28038b8 update: Play poison sound only to player 2025-11-02 19:47:22 -08:00
MSWS
b253d8ee12 fix: InvalidateOrder not purging lastUpdate map 2025-11-02 01:23:31 -08:00
MSWS
02575b51e2 Make all rounds give 1 karma 2025-11-02 01:19:24 -08:00
MSWS
d8d365b497 update: Body paint price to match (30) 2025-11-02 01:17:27 -08:00
MSWS
1ac38dc0ad update: Reduce min requirement for special round 8 -> 5 2025-11-02 01:28:16 -07:00
MSWS
62e57ffa97 fix: Reduce karma grants per round 2025-11-02 01:22:06 -07:00
MSWS
81e6b2e695 revert: Remove unnecessary delay in MapHook 2025-11-02 01:18:13 -07:00
MSWS
2ce0457346 fix: Players identifying themselves (resolves #164) 2025-11-01 19:59:35 -07:00
MSWS
ed90c54e53 update: Update suppressed round to only suppress pistols 2025-11-01 19:58:13 -07:00
MSWS
06d2d71f76 feat: Add suppressed and silent rounds, close shop after purchase 2025-11-01 18:59:54 -07:00
MSWS
c6ba041a6b Adjust detective ratio 2025-11-01 18:19:40 -07:00
MSWS
f283d7407e update: Increase speedround limit 2025-11-01 17:56:10 -07:00
MSWS
51ff4df545 update: Reduce camouflage visibility 2025-11-01 17:42:10 -07:00
MSWS
e0ee4bf325 fix: Don't give glocks by default 2025-11-01 17:21:16 -07:00
MSWS
4a4c7e0782 Reduce cost of body paint 2025-11-01 17:00:41 -07:00
MSWS
d4f67ced0c Reduce cost of compasses 2025-11-01 16:41:12 -07:00
MSWS
33ca0c8385 update: Allow inspect button to be used alongside use 2025-10-31 20:10:46 -07:00
MSWS
ff2e97a3ce update: Don't play hurt sound if Traitor is shifting 2025-10-31 20:06:19 -07:00
MSWS
a56cdc1285 Reformat and Cleanup 2025-10-31 20:01:20 -07:00
MSWS
ceda5cba64 fix: Roles not being assigned on first round of map 2025-10-31 19:59:09 -07:00
MSWS
7c203bcd91 update: Increase prices of stickers and healthshot, impl CS2 healthshot config 2025-10-31 19:53:46 -07:00
MSWS
f91fc54897 update: Increase speedround weight +semver:patch 2025-10-31 18:41:07 -07:00
MSWS
7ca4a6bef4 Update README 2025-10-30 21:46:49 -07:00
MSWS
d589a222c8 Format and Cleanup, make Ts sound like they're being damaged by hurtstation 2025-10-30 21:44:00 -07:00
MSWS
a3fdb590fd fix: Fake traitors getting damaged by hurt stations 2025-10-30 21:00:55 -07:00
MSWS
987df197bc Fix concurrent dictionary issue 2025-10-30 18:14:27 -07:00
MSWS
acb3be9132 Require on actual team to be alive 2025-10-30 18:09:14 -07:00
MSWS
bbcc998559 Up 1 knife item 2025-10-30 17:57:00 -07:00
MSWS
56781c6ae8 More item balancing, name updating, bug fix 2025-10-30 17:56:09 -07:00
MSWS
0ca983943d Revert "Fetch playername from object if available"
This reverts commit 8cd8e14e18.
2025-10-29 16:28:56 -07:00
MSWS
8cd8e14e18 Fetch playername from object if available 2025-10-29 15:27:13 -07:00
MSWS
57ef5e3e24 Use pretty name for rtd reward description 2025-10-28 20:31:04 -07:00
MSWS
9c99d316aa Revert "Revert "fix: Allow typing if dead even with muted roll""
This reverts commit e679c5193b.
2025-10-28 19:26:57 -07:00
MSWS
e679c5193b Revert "fix: Allow typing if dead even with muted roll"
This reverts commit daa24a0e87.
2025-10-28 19:26:43 -07:00
MSWS
6ece0450bb fix: Reduce volume of health station 2025-10-28 19:26:26 -07:00
MSWS
daa24a0e87 fix: Allow typing if dead even with muted roll 2025-10-28 19:20:42 -07:00
MSWS
1c8785b388 Add reminder for vanilla rounds 2025-10-28 18:50:09 -07:00
MSWS
a80c36e3c5 Suppress damage stats 2025-10-28 15:29:10 -07:00
MSWS
ba6b6c448f Change currency name to point 2025-10-28 14:06:29 -07:00
MSWS
d30e916319 Fix role name cleaning 2025-10-28 13:47:43 -07:00
MSWS
9f275fa189 Slight tweaks to logging 2025-10-28 13:39:02 -07:00
MSWS
40bdcac4b0 fix: Address copilot concerns 2025-10-28 13:30:19 -07:00
MSWS
6b3ae03ab3 Require both roles for BAD ACTION 2025-10-28 13:23:37 -07:00
MSWS
9e8e1d1fb0 Add prefix to death action 2025-10-28 13:22:53 -07:00
MSWS
fba875f098 Use prefix 2025-10-28 13:21:40 -07:00
MSWS
38fa801c15 feat: Teleport decoy working (resolves #153) 2025-10-28 13:19:29 -07:00
MSWS
b21fed3ff8 Start work on teleport decoy 2025-10-28 13:03:12 -07:00
MSWS
0e02d66350 Add configs 2025-10-28 12:39:37 -07:00
MSWS
7f6ac62348 fix: Karma events not dispatching 2025-10-28 12:28:26 -07:00
MSWS
f44e57215f fix: Special Round weights 2025-10-28 12:15:42 -07:00
MSWS
ce48f5a5ac Add logs for RTD as well 2025-10-28 12:11:54 -07:00
MSWS
4f258e55dd Log tasers 2025-10-28 11:45:43 -07:00
MSWS
b0ba6baac9 Use contexts 2025-10-28 11:30:46 -07:00
MSWS
4d052f31c6 Revert "Wonky fix for CS specific issues"
This reverts commit 2351ec55ec.
2025-10-28 10:58:51 -07:00
MSWS
7c5e7c3f68 Revert "Updates"
This reverts commit b85a3a415d.
2025-10-28 10:58:50 -07:00
MSWS
23e08134c8 Revert "Add debug"
This reverts commit 70c416bbe6.
2025-10-28 10:58:49 -07:00
MSWS
16d9335292 Revert "More work"
This reverts commit 840e04fd71.
2025-10-28 10:58:47 -07:00
MSWS
840e04fd71 More work 2025-10-28 09:40:57 -07:00
MSWS
70c416bbe6 Add debug 2025-10-28 07:31:29 -07:00
MSWS
b85a3a415d Updates 2025-10-27 23:10:05 -07:00
MSWS
2351ec55ec Wonky fix for CS specific issues 2025-10-27 22:34:35 -07:00
MSWS
4af1be95f4 Working 2025-10-27 22:10:22 -07:00
MSWS
fdb22c1090 Try using logic relay 2025-10-27 21:50:19 -07:00
MSWS
ddc309927f Add context as well for now 2025-10-27 20:51:55 -07:00
MSWS
7b657b1595 Working gamedata method 2025-10-27 20:22:13 -07:00
MSWS
05eed34ffd Try gamedata approach 2025-10-27 19:44:38 -07:00
MSWS
707a967445 Use contexts 2025-10-27 19:10:31 -07:00
MSWS
d6c6562d32 Additional safety check 2025-10-27 17:31:59 -07:00
MSWS
64e9332fa6 Add safety checks to avoid stuck states 2025-10-27 17:23:33 -07:00
MSWS
536f0eafb5 Merge branch 'dev' of github.com:MSWS/TTT into dev 2025-10-27 17:08:08 -07:00
MSWS
3e513cb611 Enhanced debugging 2025-10-27 17:08:02 -07:00
MSWS
cba8470f09 Increment version +semver:minor 2025-10-26 23:17:08 -07:00
MSWS
275404582f Merge branch 'main' into dev 2025-10-26 23:16:34 -07:00
MSWS
76dc717a8b Add settarget debug 2025-10-26 20:16:32 -07:00
Isaac
ff8d4dfc7e fix: Dont afk enforce unalive players (#151) 2025-10-26 18:50:36 -07:00
MSWS
f63acf24c4 Dont afk enforce unalive players 2025-10-26 18:43:03 -07:00
Isaac
678b9b0de6 fix: Map changing causing bugs (#150) 2025-10-26 18:35:05 -07:00
MSWS
18144f5827 Merge branch 'dev' of github.com:MSWS/TTT into dev 2025-10-26 18:21:29 -07:00
MSWS
e590ae2b7a Register map change handlers 2025-10-26 18:21:22 -07:00
Isaac
6bc0f57bed Bump actions/upload-artifact from 4 to 5 (#148)
Bumps
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 4 to 5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/upload-artifact/releases">actions/upload-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<p><strong>BREAKING CHANGE:</strong> this update supports Node
<code>v24.x</code>. This is not a breaking change per-se but we're
treating it as such.</p>
<ul>
<li>Update README.md by <a
href="https://github.com/GhadimiR"><code>@​GhadimiR</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/681">actions/upload-artifact#681</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/712">actions/upload-artifact#712</a></li>
<li>Readme: spell out the first use of GHES by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/727">actions/upload-artifact#727</a></li>
<li>Update GHES guidance to include reference to Node 20 version by <a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
in <a
href="https://redirect.github.com/actions/upload-artifact/pull/725">actions/upload-artifact#725</a></li>
<li>Bump <code>@actions/artifact</code> to <code>v4.0.0</code></li>
<li>Prepare <code>v5.0.0</code> by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/734">actions/upload-artifact#734</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/GhadimiR"><code>@​GhadimiR</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/681">actions/upload-artifact#681</a></li>
<li><a href="https://github.com/nebuk89"><code>@​nebuk89</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/712">actions/upload-artifact#712</a></li>
<li><a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/727">actions/upload-artifact#727</a></li>
<li><a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/725">actions/upload-artifact#725</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v5.0.0">https://github.com/actions/upload-artifact/compare/v4...v5.0.0</a></p>
<h2>v4.6.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Update to use artifact 2.3.2 package &amp; prepare for new
upload-artifact release by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/685">actions/upload-artifact#685</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/685">actions/upload-artifact#685</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v4.6.2">https://github.com/actions/upload-artifact/compare/v4...v4.6.2</a></p>
<h2>v4.6.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Update to use artifact 2.2.2 package by <a
href="https://github.com/yacaovsnc"><code>@​yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/673">actions/upload-artifact#673</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v4.6.1">https://github.com/actions/upload-artifact/compare/v4...v4.6.1</a></p>
<h2>v4.6.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Expose env vars to control concurrency and timeout by <a
href="https://github.com/yacaovsnc"><code>@​yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/662">actions/upload-artifact#662</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v4.6.0">https://github.com/actions/upload-artifact/compare/v4...v4.6.0</a></p>
<h2>v4.5.0</h2>
<h2>What's Changed</h2>
<ul>
<li>fix: deprecated <code>Node.js</code> version in action by <a
href="https://github.com/hamirmahal"><code>@​hamirmahal</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/578">actions/upload-artifact#578</a></li>
<li>Add new <code>artifact-digest</code> output by <a
href="https://github.com/bdehamer"><code>@​bdehamer</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/656">actions/upload-artifact#656</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/hamirmahal"><code>@​hamirmahal</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/578">actions/upload-artifact#578</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="330a01c490"><code>330a01c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/734">#734</a>
from actions/danwkennedy/prepare-5.0.0</li>
<li><a
href="03f2824452"><code>03f2824</code></a>
Update <code>github.dep.yml</code></li>
<li><a
href="905a1ecb59"><code>905a1ec</code></a>
Prepare <code>v5.0.0</code></li>
<li><a
href="2d9f9cdfa9"><code>2d9f9cd</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/725">#725</a>
from patrikpolyak/patch-1</li>
<li><a
href="9687587dec"><code>9687587</code></a>
Merge branch 'main' into patch-1</li>
<li><a
href="2848b2cda0"><code>2848b2c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/727">#727</a>
from danwkennedy/patch-1</li>
<li><a
href="9b511775fd"><code>9b51177</code></a>
Spell out the first use of GHES</li>
<li><a
href="cd231ca1ed"><code>cd231ca</code></a>
Update GHES guidance to include reference to Node 20 version</li>
<li><a
href="de65e23aa2"><code>de65e23</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/712">#712</a>
from actions/nebuk89-patch-1</li>
<li><a
href="8747d8cd76"><code>8747d8c</code></a>
Update README.md</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/upload-artifact/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>
2025-10-26 18:11:56 -07:00
Isaac
83a1d0e3e3 Bump actions/download-artifact from 5 to 6 (#149)
Bumps
[actions/download-artifact](https://github.com/actions/download-artifact)
from 5 to 6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/download-artifact/releases">actions/download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>What's Changed</h2>
<p><strong>BREAKING CHANGE:</strong> this update supports Node
<code>v24.x</code>. This is not a breaking change per-se but we're
treating it as such.</p>
<ul>
<li>Update README for download-artifact v5 changes by <a
href="https://github.com/yacaovsnc"><code>@​yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/417">actions/download-artifact#417</a></li>
<li>Update README with artifact extraction details by <a
href="https://github.com/yacaovsnc"><code>@​yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/424">actions/download-artifact#424</a></li>
<li>Readme: spell out the first use of GHES by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/431">actions/download-artifact#431</a></li>
<li>Bump <code>@actions/artifact</code> to <code>v4.0.0</code></li>
<li>Prepare <code>v6.0.0</code> by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/438">actions/download-artifact#438</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/download-artifact/pull/431">actions/download-artifact#431</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/download-artifact/compare/v5...v6.0.0">https://github.com/actions/download-artifact/compare/v5...v6.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="018cc2cf5b"><code>018cc2c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/438">#438</a>
from actions/danwkennedy/prepare-6.0.0</li>
<li><a
href="815651c680"><code>815651c</code></a>
Revert &quot;Remove <code>github.dep.yml</code>&quot;</li>
<li><a
href="bb3a066a8b"><code>bb3a066</code></a>
Remove <code>github.dep.yml</code></li>
<li><a
href="fa1ce46bbd"><code>fa1ce46</code></a>
Prepare <code>v6.0.0</code></li>
<li><a
href="4a24838f3d"><code>4a24838</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/431">#431</a>
from danwkennedy/patch-1</li>
<li><a
href="5e3251c4ff"><code>5e3251c</code></a>
Readme: spell out the first use of GHES</li>
<li><a
href="abefc31eaf"><code>abefc31</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/424">#424</a>
from actions/yacaovsnc/update_readme</li>
<li><a
href="ac43a6070a"><code>ac43a60</code></a>
Update README with artifact extraction details</li>
<li><a
href="de96f4613b"><code>de96f46</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/417">#417</a>
from actions/yacaovsnc/update_readme</li>
<li><a
href="7993cb44e9"><code>7993cb4</code></a>
Remove migration guide for artifact download changes</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/download-artifact/compare/v5...v6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/download-artifact&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>
2025-10-26 18:11:48 -07:00
dependabot[bot]
4d0fdfa25e Bump actions/download-artifact from 5 to 6
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 00:26:03 +00:00
dependabot[bot]
9f673f9d8b Bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 00:25:38 +00:00
MSWS
3ad4339073 Buff bodypaint 2025-10-25 21:14:27 -07:00
Isaac
890ba71fdf Additional balancing (#147) 2025-10-25 02:50:17 -07:00
MSWS
c6fea1a21e Balances 2025-10-25 02:43:50 -07:00
MSWS
db9ad9303f Reduce M4 cost, reduce station health 2025-10-25 02:25:11 -07:00
Isaac
9224e823c0 Game balances (#146) 2025-10-25 02:05:03 -07:00
MSWS
4ab16d71db Handle map change interrupting rounds 2025-10-25 01:59:16 -07:00
MSWS
627c048183 Delay round effects for speedround due to race condition 2025-10-25 01:47:55 -07:00
MSWS
66d1106d4c Increase time, nerf camo and silent awp 2025-10-25 01:02:46 -07:00
Isaac
39c7a4762d Dev (#145) 2025-10-24 20:08:47 -07:00
MSWS
f2352ede1f Merge branch 'dev' of github.com:MSWS/TTT into dev 2025-10-24 20:06:20 -07:00
MSWS
f675a87ffd Additional hotpatch 2025-10-24 20:06:04 -07:00
Isaac
7f455d5354 Hotpatch 2025-10-24 19:19:44 -07:00
Isaac
d1972fc556 Merge branch 'main' into dev 2025-10-24 19:12:35 -07:00
MSWS
f2dbc72aee Use squared distance 2025-10-24 19:09:45 -07:00
MSWS
77a2289367 Reset special round on round end 2025-10-24 19:08:55 -07:00
Isaac
456ae22b12 feat: Auto RTD (#142) 2025-10-24 18:23:48 -07:00
Isaac
2616b231dc fix: Use Locale (#141) 2025-10-24 18:20:04 -07:00
MSWS
3cb86aa2f8 Working Auto RTD 2025-10-24 18:19:02 -07:00
MSWS
4c72f3dfff Fix duplicate spaces 2025-10-24 18:00:30 -07:00
MSWS
bc45f3fb74 fix: Use Locale 2025-10-24 17:58:15 -07:00
Isaac
2bcf436677 feat: Auto RTD (resolves #140) (#139) 2025-10-24 17:41:08 -07:00
Isaac
9dfb45583b feat: Special Rounds (resolves #133) (#138) 2025-10-24 17:36:55 -07:00
MSWS
3fa1558011 feat: Auto RTD 2025-10-24 17:36:36 -07:00
MSWS
6c7bc22395 Add vanilla round 2025-10-24 17:06:35 -07:00
MSWS
c95fba0fc5 Send messages 2025-10-24 16:44:09 -07:00
Isaac
40c7a6d471 Fix services (#137) 2025-10-24 16:03:35 -07:00
MSWS
b79519f6b4 Fix services 2025-10-24 16:01:56 -07:00
Isaac
bea87d20f3 Feat/special rounds (#136) 2025-10-24 15:23:48 -07:00
MSWS
5bbc621d86 Fix circular dependencies 2025-10-24 15:21:48 -07:00
MSWS
5ed244f84c Add list of special rounds to feedback 2025-10-24 15:16:54 -07:00
MSWS
31d2354e6f Basic work for special rounds 2025-10-24 15:16:03 -07:00
MSWS
44d9644694 Start work on special rounds 2025-10-24 01:38:14 -07:00
Isaac
f6d1b95a38 Update scope management (#135) 2025-10-22 16:02:12 -07:00
MSWS
cf1d040b44 Update scope management 2025-10-22 15:58:55 -07:00
Isaac
5393920f95 Bug Fixes, Stats API +semver:minor (#134) 2025-10-22 09:18:27 -07:00
MSWS
8158206101 Increase unit test delay again 2025-10-22 09:15:15 -07:00
MSWS
06f8f083df fix: Services typo 2025-10-22 09:12:18 -07:00
Isaac
5c18a046b8 Update TTT/Stats/StatsServiceCollection.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-10-22 09:11:14 -07:00
Isaac
063813baca Merge branch 'main' into dev 2025-10-22 09:09:57 -07:00
MSWS
a10ce25846 Additional api updates 2025-10-22 03:05:36 -07:00
MSWS
89077c8361 Add api feedback 2025-10-22 01:12:43 -07:00
MSWS
fd3ffc6d59 Revert "Add api feedback"
This reverts commit f9e2734390.
2025-10-22 01:11:55 -07:00
MSWS
f9e2734390 Add api feedback 2025-10-22 01:11:43 -07:00
MSWS
251d8efeaf Add api feedback 2025-10-22 01:04:45 -07:00
MSWS
e83fbdd3fe Working basic API implementation 2025-10-22 00:51:17 -07:00
MSWS
99742efc5b Move stats into its own project: 2025-10-21 21:26:53 -07:00
MSWS
c802f468ed Test out new api 2025-10-21 20:49:27 -07:00
MSWS
43becefb0a Reduce round time 2025-10-20 23:33:24 -07:00
MSWS
f8f2617b09 Fix detective win condition never being met 2025-10-20 23:23:32 -07:00
MSWS
3eb59dec13 Clear role icons on new round instead of on round end 2025-10-20 23:14:58 -07:00
MSWS
792a737102 Update AliveSpoofer dispose handling 2025-10-20 22:58:51 -07:00
MSWS
85dd4edb08 Suppress damage stats 2025-10-20 21:25:07 -07:00
MSWS
2f78a62385 Update credits reward description 2025-10-20 21:16:28 -07:00
Isaac
247f7de49b Shop Balancing, AFK Management, Exploration based rewards (resolves #116) (#132) 2025-10-20 20:08:20 -07:00
MSWS
c523a9f015 Fix afk message 2025-10-20 20:04:52 -07:00
MSWS
8363265e39 Merge branch 'dev' of github.com:MSWS/TTT into dev 2025-10-20 20:04:43 -07:00
MSWS
f3363da9bb Cleanup AfkTimerListener 2025-10-20 19:54:28 -07:00
Isaac
ec2355eb6d Update TTT/Shop/Items/Healthshot/HealthshotItem.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-10-20 19:50:17 -07:00
MSWS
fd14728a27 Fix lifecycle management issue 2025-10-20 19:44:10 -07:00
MSWS
556657d249 Remove description from shop listings 2025-10-20 19:38:36 -07:00
MSWS
26c5d0e367 Fix additional message formatting 2025-10-20 19:37:45 -07:00
MSWS
536662a500 Remove broken unit test 2025-10-20 19:33:27 -07:00
MSWS
64889de2fa Clarify module classname 2025-10-20 19:32:31 -07:00
MSWS
7a6676e6ac Fix reload module 2025-10-20 19:30:00 -07:00
MSWS
49227e6d37 fix: Update RTD Credit reward to use locale 2025-10-20 19:27:37 -07:00
MSWS
0748114c9a Merge branch 'main' into dev 2025-10-20 18:57:55 -07:00
MSWS
346984f394 Update colors 2025-10-20 18:56:58 -07:00
MSWS
6316f39819 refactor: Refactor config initialization pattern
Refactor configuration initialization and improve code readability across multiple modules

- **GameHandlers/RoundStart_GameStartHandler.cs**: Change `config` initialization to a property to streamline retrieval and improve formatting for better readability.
- **Shop/Items/Traitor/C4/C4ShopItem.cs**: Refactor `config` to enable lazy loading through the Service Provider and remove the `readonly` keyword.
- **Karma/KarmaStorage.cs**: Simplify code by removing the lazy-loaded property `_configStorage` and directly using `_config`.
- **Game/RoundBasedGame.cs**: Implement lazy loading for configuration retrieval, update player initialization logic, and improve method structure for clarity.
- **Shop/Listeners/RoleAssignCreditor.cs**: Change `config` to a read-only property for lazy loading and enhance performance with delayed instantiation.
2025-10-20 18:52:44 -07:00
MSWS
2d572e19b0 Improve feedback on module reload command 2025-10-20 18:46:24 -07:00
MSWS
e4938502f4 feat: Introduce AFK detection and reward enhancements +semver:minor
Implement AFK Management and Enhance Reward and Purchase Systems

- **TTTConfig.cs**: Add `CheckAFKTimespan` configuration to manage player inactivity during game rounds.
- **HealthshotConfig.cs**: Introduce `MaxPurchases` property to limit healthshot item usage per player.
- **Command/Test/TestCommand.cs**: Implement "reload" sub-command with permission checks for restricted execution.
- **CS2ServiceCollection.cs**: Integrate `AfkTimerListener` for handling inactive players and remove conditional compilation for `TestCommand`.
- **Listeners/AfkTimerListener.cs**: Develop an AFK detection system, moving idle players to spectator mode and issuing warnings.

**Additional updates:**

- **ReloadModule.cs**: Implement class to handle reloading of modules with user feedback and error handling.
- **CS2/lang/CS2Msgs.cs**: Add message templates for AFK warnings and notifications.
- **RoundTimerListener.cs**: Streamline TTTConfig access and remove redundant scheduler handling.
- **TeamChangeHandler.cs**: Enhance team change logic with new dependencies and player checks.
- **ShopConfig.cs**: Rework reward distribution system, introducing flexible reward ranges and removing the old fixed interval configuration.
- **HealthshotItem.cs**: Implement purchase tracking and finalize configurations for purchase limits.
- **PeriodicRewarder.cs**: Split reward and update timers, integrate player position tracking, and enhance reward calculation logic based on player movement.
- **GameHandlers/LateSpawnListener.cs**: Add game state checks to improve player respawn logic during specific states.
2025-10-20 18:44:22 -07:00
MSWS
e59b2538ee Dont duplicate death events, buff poison shots 2025-10-20 17:18:53 -07:00
MSWS
7454e5e3f3 feat: Enhance CamoConfig and update role logic +semver:minor
- Increase the price of camo configuration in `CamoConfig.cs` from 55 to 75
- Add `CS2CamoConfig` behavior to `CS2ServiceCollection.cs` for extended configuration options
- Update logic in `PlayerKillListener.cs` to enhance role-based kill classification by checking differing roles
- Introduce `CS2CamoConfig.cs` with configuration variables for camo items and player visibility
- Adjust starting credits in `CS2ShopConfig.cs` for Innocents, Traitors, and Detectives
- Reduce interval reward amount for credits in `ShopConfig.cs` from 8 to 5
2025-10-20 17:09:43 -07:00
Isaac
bdef55428c Balance Changes, make Configs Hot Load 2025-10-19 22:15:54 -07:00
MSWS
4ce453dccd Buff gloves 2025-10-19 22:14:00 -07:00
MSWS
31f1403b9b Bump one shot cost 2025-10-19 22:13:28 -07:00
MSWS
d12cfa5eab Reduce credits given 2025-10-19 22:09:51 -07:00
MSWS
9022416053 refactor: Refactor config init to use expression-bodied properties
Refactor configuration initialization for improved code readability and maintainability

- Update `PoisonSmokeListener.cs` to use a property for `PoisonSmokeConfig` initialization, adding conditional access and null-coalescing logic.
- Adjust `KarmaConfig.cs` to reduce karma gain values, affecting end-of-round and winning scenarios.
- Refactor `HealthshotItem.cs`, using an expression-bodied property for `config` to enhance code clarity.
- Enhance `ArmorItem.cs` with lazy loading for `ArmorConfig` by transitioning `config` to a property using an expression-bodied member.
- Modify `PeriodicRewarder.cs` to initialize `ShopConfig` using a property, ensuring fallback configuration with unchanged core logic.

Other file changes focus on transitioning configuration retrieval to properties, promoting lazy loading and streamlined expressions across items and listeners, thereby refining consistency and readability throughout the codebase.
2025-10-19 21:51:09 -07:00
MSWS
9f45b919e1 Fix ragdolls misbehaving after multiple carriers 2025-10-19 21:29:36 -07:00
MSWS
7d75b867f9 Merge branch 'feat/rtd' 2025-10-19 19:06:54 -07:00
MSWS
35b15c4578 Inherit from IListener 2025-10-19 18:42:10 -07:00
Isaac
9b99adca3f feat: RTD System (#130) 2025-10-19 18:18:41 -07:00
MSWS
3802610b1c feat: Implement player muting system for gameplay control
```markdown
Implement player muting feature and enhance game mechanics

- Add `RTD_MUTED` message in `RtdMsgs.cs` utilizing the existing message creation method.
- Enhance `PlayerMuter.cs` by checking game state before muting players and implementing event handler to unmute players when the game finishes.
- Update `TraitorChatHandler.cs` to handle muted players in chat and prevent dead players from participating in traitor chat.
- Introduce `MuteReward` class in `MuteReward.cs` to mute players for the next round and manage muted player voice events.
- Modify `RewardGenerator.cs` to include `MuteReward` and adjust reward probabilities, enhancing the reward system.

These changes collectively introduce and integrate a system for managing player muting, ensuring robust gameplay dynamics and clarity for players during active game sessions.
```
2025-10-19 18:16:38 -07:00
MSWS
171250382e feat: Introduce RTD feature with reward system +semver:minor
Introduce RTD Project and Enhance Codebase with Localization and Config Improvements

- Introduce a new RTD project with multiple enhancements to reward and command systems:
  - Add new interfaces and classes for `IRtdReward`, `IRewardGenerator`, and several types of rewards like `ShopItemReward`, `CreditReward`, `HealthReward`, and `WeaponReward`.
  - Implement new command functionality through `RtdCommand` and `RtdStatsCommand`.
- Strengthen the code architecture by refactoring configuration access:
  - Convert `config` fields to properties using expression-bodied members across various items and listeners, promoting improved readability and potential lazy loading.
- Integrate localization features:
  - Add and standardize import statements for `TTT.Game.lang` to support upcoming language and localization developments across different game modules.
  - Create new language configuration files, like `en.yml`, and introduce classes such as `RtdMsgs` for localized message handling.
- Improve game mechanics:
  - Enhance poison effect handling within `PoisonShotsListener` with periodic damage application and improved player interaction updates.
  - Extend the `IIconManager` to offer additional player visibility options, enhancing game dynamics through methods like `RevealToAll`.
- Optimize plugin and module management:
  - Add logging features with `ShopPurchaseLogger` and new logging statements for plugin module registration.
  - Ensure cohesive project structure by updating project files and solution configuration for the new RTD module.
- Refactor utility and helper functions for better clarity:
  - Introduce utility classes like `WeaponTranslations` to map internal to user-friendly weapon names.
  - Clean up and streamline namespaces for clarity and consistency, especially within utilities like `GrenadeDataHelper`.

This commit collectively enhances the system's modularity, readability, and capability for future localization and extensibility.
2025-10-19 17:45:58 -07:00
MSWS
6524772d4f Remove player on disconnect 2025-10-19 16:11:13 -07:00
MSWS
bd8125b7a0 Prevent traitor chat metagming 2025-10-19 15:51:09 -07:00
Isaac
9c693059ea Revert "Refresh AliveSpoofer per map" (#129)
This reverts commit 9d3ecbe7fb.
2025-10-19 15:42:57 -07:00
MSWS
695d34c10c Revert "Refresh AliveSpoofer per map"
This reverts commit 9d3ecbe7fb.
2025-10-19 15:35:00 -07:00
Isaac
05aeb53a3c Refresh AliveSpoofer per map (#127) 2025-10-18 01:18:30 -07:00
MSWS
9d3ecbe7fb Refresh AliveSpoofer per map 2025-10-18 01:16:51 -07:00
MSWS
85dac3622a Merge branch 'dev' of github.com:MSWS/TTT into dev 2025-10-17 23:20:28 -07:00
MSWS
9e4c29e3f7 Bump taser cost 2025-10-17 23:20:22 -07:00
Isaac
c545a10d6f Bug and crash fixes (#126) 2025-10-17 22:05:21 -07:00
Isaac
453ba14126 Update TTT/CS2/Items/PoisonShots/PoisonShotsListener.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-10-17 22:02:16 -07:00
Isaac
91750a1067 Update TTT/CS2/Items/Station/DamageStation.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-10-17 22:00:44 -07:00
Isaac
dd6b8c00fe Update TTT/CS2/Utils/DamageDealingHelper.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-10-17 22:00:38 -07:00
MSWS
d9ad08aa27 Improve alive checks 2025-10-17 21:55:36 -07:00
MSWS
35191f23e1 refactor: Refactor data structures for kill tracking +semver:patch
- Change `killedWithStation` data structure to `Dictionary` for enhanced player interaction tracking in `DamageStation.cs`
- Update `PoisonShotsListener.cs` to use `Dictionary` for poison kill tracking and adjust related logic
- Specify priority levels for event handlers in `GlovesListener.cs` to optimize execution order
2025-10-17 21:45:25 -07:00
MSWS
ad29de1bc5 Revert 2025-10-17 21:33:19 -07:00
MSWS
0a0416bff0 Try using native damage dealing method 2025-10-17 20:28:33 -07:00
MSWS
62c96123d1 Remove verbose debug module: 2025-10-17 19:18:51 -07:00
MSWS
274716267f Add null checks to body spawner 2025-10-16 16:24:41 -07:00
MSWS
c20842575b Merge branch 'dev' 2025-10-16 16:01:00 -07:00
MSWS
cf8169a10e Disable TeamChangeHandler for now 2025-10-16 15:15:05 -07:00
Isaac
3dcc3a7de5 Item Rebalancing, Karma Updates, New Compass, Cluster Grenade | Bug Fixes (#125)
This PR implements a comprehensive set of game balancing changes, bug fixes, and new features for a Trouble in Terrorist Town (TTT) game mode in Counter-Strike 2.

Key Changes:

    Shop item pricing rebalance: Adjusted prices across multiple traitor and detective items to improve game economy balance
    New cluster grenade item: Added a new traitor shop item that splits into multiple grenades on detonation
    Compass system refactor: Split the single compass into two separate items (player compass and body compass) with a shared abstract base class
    Karma system improvements: Updated karma calculation values and added proper storage/disposal patterns
    Bug fixes: Fixed damage application, ragdoll spawning, and team change handling issues
2025-10-16 13:38:34 -07:00
MSWS
65bcafca79 Extra extra delay 2025-10-16 13:33:12 -07:00
MSWS
6cac535e94 Additional unit testing adjustments 2025-10-16 13:24:06 -07:00
225 changed files with 4054 additions and 681 deletions

View File

@@ -38,7 +38,7 @@ jobs:
- name: Publish Test Project
run: dotnet publish TTT/Test/Test.csproj --no-restore --no-build -o build_output -c Debug
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: build_output
path: build_output
@@ -59,7 +59,7 @@ jobs:
dotnet-version: '8.0.x'
- name: Download Build Output
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
name: build_output
path: build_output

View File

@@ -16,7 +16,9 @@ survive while eliminating the traitors among them.
- [X] Innocents
- [X] Shop
- [X] Karma
- [ ] Statistics
- [X] Statistics
- [X] Map Integrations
- [X] Special Rounds
## Versioning

View File

@@ -0,0 +1,26 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using SpecialRoundAPI.Configs;
using TTT.API;
using TTT.API.Events;
using TTT.Game.Events.Game;
using TTT.Game.Listeners;
using TTT.Locale;
namespace SpecialRoundAPI;
public abstract class AbstractSpecialRound(IServiceProvider provider)
: BaseListener(provider) {
protected readonly ISpecialRoundTracker Tracker =
provider.GetRequiredService<ISpecialRoundTracker>();
public abstract string Name { get; }
public abstract IMsg Description { get; }
public abstract SpecialRoundConfig Config { get; }
public abstract void ApplyRoundEffects();
[UsedImplicitly]
[EventHandler]
public abstract void OnGameState(GameStateUpdateEvent ev);
}

View File

@@ -0,0 +1,5 @@
namespace SpecialRoundAPI.Configs;
public record BhopRoundConfig : SpecialRoundConfig {
public override float Weight { get; init; } = 0.2f;
}

View File

@@ -0,0 +1,5 @@
namespace SpecialRoundAPI.Configs;
public record SilentRoundConfig : SpecialRoundConfig {
public override float Weight { get; init; } = 0.1f;
}

View File

@@ -0,0 +1,5 @@
namespace SpecialRoundAPI.Configs;
public abstract record SpecialRoundConfig {
public abstract float Weight { get; init; }
}

View File

@@ -0,0 +1,9 @@
namespace SpecialRoundAPI.Configs;
public record SpeedRoundConfig : SpecialRoundConfig {
public override float Weight { get; init; } = 0.6f;
public TimeSpan InitialSeconds { get; init; } = TimeSpan.FromSeconds(40);
public TimeSpan SecondsPerKill { get; init; } = TimeSpan.FromSeconds(10);
public TimeSpan MaxTimeEver { get; init; } = TimeSpan.FromSeconds(90);
}

View File

@@ -0,0 +1,5 @@
namespace SpecialRoundAPI.Configs;
public record SuppressedRoundConfig : SpecialRoundConfig {
public override float Weight { get; init; } = 0.3f;
}

View File

@@ -0,0 +1,5 @@
namespace SpecialRoundAPI.Configs;
public record VanillaRoundConfig : SpecialRoundConfig {
public override float Weight { get; init; } = 0.2f;
}

View File

@@ -0,0 +1,13 @@
namespace SpecialRoundAPI;
public interface ISpecialRoundStarter {
/// <summary>
/// Attempts to start the given special round.
/// Will bypass most checks, but may still return null if starting the round
/// is not possible.
/// </summary>
/// <param name="round"></param>
/// <returns></returns>
public AbstractSpecialRound?
TryStartSpecialRound(AbstractSpecialRound? round);
}

View File

@@ -0,0 +1,6 @@
namespace SpecialRoundAPI;
public interface ISpecialRoundTracker {
public AbstractSpecialRound? CurrentRound { get; set; }
public int RoundsSinceLastSpecial { get; set; }
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Locale\Locale.csproj"/>
<ProjectReference Include="..\..\TTT\API\API.csproj"/>
<ProjectReference Include="..\..\TTT\Game\Game.csproj"/>
</ItemGroup>
</Project>

24
TTT.sln
View File

@@ -23,6 +23,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Karma", "TTT\Karma\Karma.cs
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShopAPI", "TTT\ShopAPI\ShopAPI.csproj", "{16F720B5-9D45-47BF-8C80-4F91005E36D1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RTD", "TTT\RTD\RTD.csproj", "{8A426E84-45DA-4558-A218-E042F1AC60B2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stats", "TTT\Stats\Stats.csproj", "{256473A2-6ACD-440C-83FA-6056147656C7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpecialRound", "TTT\SpecialRound\SpecialRound.csproj", "{5092069A-3CFA-41C8-B685-341040AB435C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpecialRoundAPI", "SpecialRoundAPI\SpecialRoundAPI\SpecialRoundAPI.csproj", "{360FEF16-54DA-42EE-995A-3D31C699287D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -72,6 +80,22 @@ Global
{16F720B5-9D45-47BF-8C80-4F91005E36D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{16F720B5-9D45-47BF-8C80-4F91005E36D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{16F720B5-9D45-47BF-8C80-4F91005E36D1}.Release|Any CPU.Build.0 = Release|Any CPU
{8A426E84-45DA-4558-A218-E042F1AC60B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A426E84-45DA-4558-A218-E042F1AC60B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A426E84-45DA-4558-A218-E042F1AC60B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A426E84-45DA-4558-A218-E042F1AC60B2}.Release|Any CPU.Build.0 = Release|Any CPU
{256473A2-6ACD-440C-83FA-6056147656C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{256473A2-6ACD-440C-83FA-6056147656C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{256473A2-6ACD-440C-83FA-6056147656C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{256473A2-6ACD-440C-83FA-6056147656C7}.Release|Any CPU.Build.0 = Release|Any CPU
{5092069A-3CFA-41C8-B685-341040AB435C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5092069A-3CFA-41C8-B685-341040AB435C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5092069A-3CFA-41C8-B685-341040AB435C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5092069A-3CFA-41C8-B685-341040AB435C}.Release|Any CPU.Build.0 = Release|Any CPU
{360FEF16-54DA-42EE-995A-3D31C699287D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{360FEF16-54DA-42EE-995A-3D31C699287D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{360FEF16-54DA-42EE-995A-3D31C699287D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{360FEF16-54DA-42EE-995A-3D31C699287D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection

View File

@@ -11,6 +11,7 @@ public interface IAction {
string Id { get; }
string Verb { get; }
string Details { get; }
string Prefix => "";
public string Format() {
var pRole = PlayerRole != null ?
@@ -20,7 +21,7 @@ public interface IAction {
$" [{OtherRole.Name.First(char.IsAsciiLetter)}]" :
"";
return Other is not null ?
$"{Player}{pRole} {Verb} {Other}{oRole} {Details}" :
$"{Player}{pRole} {Verb} {Details}";
$"{Prefix}{Player}{pRole} {Verb} {Other}{oRole} {Details}" :
$"{Prefix}{Player}{pRole} {Verb} {Details}";
}
}

View File

@@ -18,4 +18,6 @@ public interface IActionLogger {
void PrintLogs();
void PrintLogs(IOnlinePlayer? player);
string[] MakeLogs();
}

View File

@@ -12,6 +12,7 @@ public interface IIconManager {
void AddVisiblePlayer(int client, int player);
void RemoveVisiblePlayer(int client, int player);
void SetVisiblePlayers(IOnlinePlayer online, ulong playersBitmask);
void RevealToAll(IOnlinePlayer online);
void ClearAllVisibility();
}

3
TTT/API/Player/IMuted.cs Normal file
View File

@@ -0,0 +1,3 @@
namespace TTT.API.Player;
public interface IMuted : ISet<string> { }

View File

@@ -7,7 +7,7 @@ public interface IPlayer : IEquatable<IPlayer> {
/// </summary>
string Id { get; }
string Name { get; }
string Name { get; set; }
bool IEquatable<IPlayer>.Equals(IPlayer? other) {
if (other is null) return false;

View File

@@ -0,0 +1,19 @@
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
namespace TTT.CS2.Actions;
public class TaserAction(IRoleAssigner roles, IPlayer victim,
IPlayer identifier) : IAction {
public IPlayer Player { get; } = identifier;
public IPlayer? Other { get; } = victim;
public IRole? PlayerRole { get; } =
roles.GetRoles(identifier).FirstOrDefault();
public IRole? OtherRole { get; } = roles.GetRoles(victim).FirstOrDefault();
public string Id { get; } = "cs2.action.tased";
public string Verb { get; } = "tased";
public string Details { get; } = "";
}

View File

@@ -9,6 +9,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\SpecialRoundAPI\SpecialRoundAPI\SpecialRoundAPI.csproj"/>
<ProjectReference Include="..\API\API.csproj"/>
<ProjectReference Include="..\Game\Game.csproj"/>
<ProjectReference Include="..\Karma\Karma.csproj"/>

View File

@@ -1,6 +1,8 @@
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Configs.Detective;
using ShopAPI.Configs.Traitor;
using TTT.API.Command;
using TTT.API.Extensions;
@@ -49,43 +51,60 @@ public static class CS2ServiceCollection {
collection
.AddModBehavior<IStorage<PoisonSmokeConfig>, CS2PoisonSmokeConfig>();
collection.AddModBehavior<IStorage<KarmaConfig>, CS2KarmaConfig>();
collection.AddModBehavior<IStorage<CamoConfig>, CS2CamoConfig>();
collection.AddModBehavior<IStorage<StickersConfig>, CS2StickersConfig>();
collection.AddModBehavior<IStorage<BodyPaintConfig>, CS2BodyPaintConfig>();
collection
.AddModBehavior<IStorage<DnaScannerConfig>, CS2DnaScannerConfig>();
collection
.AddModBehavior<IStorage<HealthStationConfig>, CS2HealthStationConfig>();
collection
.AddModBehavior<IStorage<ClusterGrenadeConfig>, CS2ClusterGrenadeConfig>();
collection.AddModBehavior<IStorage<GlovesConfig>, CS2GlovesConfig>();
collection
.AddModBehavior<IStorage<OneHitKnifeConfig>, CS2OneHitKnifeConfig>();
collection.AddModBehavior<IStorage<SilentAWPConfig>, CS2SilentAWPConfig>();
collection
.AddModBehavior<IStorage<HealthshotConfig>, CS2HealthshotConfig>();
// TTT - CS2 Specific optionals
collection.AddScoped<ITextSpawner, TextSpawner>();
// GameHandlers
collection.AddModBehavior<BodySpawner>();
collection.AddModBehavior<BombPlantSuppressor>();
collection.AddModBehavior<BuyMenuHandler>();
collection.AddModBehavior<CombatHandler>();
collection.AddModBehavior<DamageCanceler>();
collection.AddModBehavior<MapChangeCausesEndListener>();
collection.AddModBehavior<MapZoneRemover>();
collection.AddModBehavior<NameUpdater>();
collection.AddModBehavior<PlayerConnectionsHandler>();
collection.AddModBehavior<PlayerMuter>();
collection.AddModBehavior<PropMover>();
collection.AddModBehavior<RoundStart_GameStartHandler>();
collection.AddModBehavior<BombPlantSuppressor>();
collection.AddModBehavior<MapZoneRemover>();
collection.AddModBehavior<BuyMenuHandler>();
collection.AddModBehavior<TeamChangeHandler>();
collection.AddModBehavior<TraitorChatHandler>();
collection.AddModBehavior<PlayerMuter>();
// Damage Cancelers
collection.AddModBehavior<OutOfRoundCanceler>();
collection.AddModBehavior<TaserListenCanceler>();
// Listeners
collection.AddModBehavior<AfkTimerListener>();
collection.AddModBehavior<BodyPickupListener>();
collection.AddModBehavior<IBodyTracker, BodyTracker>();
collection.AddModBehavior<KarmaBanner>();
collection.AddModBehavior<KarmaSyncer>();
collection.AddModBehavior<LateSpawnListener>();
collection.AddModBehavior<MapHookListener>();
collection.AddModBehavior<PlayerStatsTracker>();
collection.AddModBehavior<RoundTimerListener>();
collection.AddModBehavior<ScreenColorApplier>();
collection.AddModBehavior<KarmaBanner>();
collection.AddModBehavior<KarmaSyncer>();
collection.AddModBehavior<WardenTagAssigner>();
// Commands
#if DEBUG
collection.AddModBehavior<TestCommand>();
collection.AddModBehavior<DebugMessage>();
#endif
collection.AddScoped<IGameManager, CS2GameManager>();
collection.AddScoped<IInventoryManager, CS2InventoryManager>();

View File

@@ -6,8 +6,8 @@ using TTT.API;
using TTT.API.Command;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.Game;
using TTT.Game.Commands;
using TTT.Game.lang;
namespace TTT.CS2.Command;

View File

@@ -41,15 +41,15 @@ public class PlayerPingShopAlias(IServiceProvider provider) : IPluginModule {
private void onButton(CCSPlayerController? player, int index) {
if (player == null) return;
if (converter.GetPlayer(player) is not IOnlinePlayer gamePlayer) return;
if (converter.GetPlayer(player) is not IOnlinePlayer apiPlayer) return;
var lastUpdated = itemSorter.GetLastUpdate(gamePlayer);
var lastUpdated = itemSorter.GetLastUpdate(apiPlayer);
if (lastUpdated == null
|| DateTime.Now - lastUpdated > TimeSpan.FromSeconds(20))
return;
var cmdInfo = new CS2CommandInfo(provider, gamePlayer, 0, "css_shop", "buy",
(index - 1).ToString());
cmdInfo.CallingContext = CommandCallingContext.Chat;
var cmdInfo = new CS2CommandInfo(provider, apiPlayer, 0, "css_shop", "buy",
(index - 1).ToString()) { CallingContext = CommandCallingContext.Chat };
provider.GetRequiredService<ICommandManager>().ProcessCommand(cmdInfo);
itemSorter.InvalidateOrder(apiPlayer);
}
}

View File

@@ -48,7 +48,6 @@ public class GiveItemCommand(IServiceProvider provider) : ICommand {
var purchaseEv = new PlayerPurchaseItemEvent(target, item);
provider.GetRequiredService<IEventBus>().Dispatch(purchaseEv);
if (purchaseEv.IsCanceled) return;
shop.GiveItem(target, item);
info.ReplySync($"Gave item '{item.Name}' to {target.Name}.");

View File

@@ -0,0 +1,62 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Command;
using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class ReloadModuleCommand(IServiceProvider provider)
: ICommand, IPluginModule {
private BasePlugin? plugin;
public void Dispose() { }
public void Start() { }
public string Id => "reload";
public string[] Usage => ["<module>"];
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (info.ArgCount != 2) return Task.FromResult(CommandResult.INVALID_ARGS);
var moduleName = info.Args[1];
var modules = provider.GetServices<ITerrorModule>();
var module = modules.FirstOrDefault(m
=> m.Id.Equals(moduleName, StringComparison.OrdinalIgnoreCase));
if (module == null) {
info.ReplySync($"Module '{moduleName}' not found.");
return Task.FromResult(CommandResult.INVALID_ARGS);
}
info.ReplySync($"Reloading module '{moduleName}'...");
module.Dispose();
info.ReplySync($"Starting module '{moduleName}'...");
module.Start();
info.ReplySync($"Module '{moduleName}' reloaded successfully.");
if (plugin == null) {
info.ReplySync("Plugin context not found; skipping hotload steps.");
return Task.FromResult(CommandResult.SUCCESS);
}
if (module is not IPluginModule pluginModule)
return Task.FromResult(CommandResult.SUCCESS);
Server.NextWorldUpdate(() => {
info.ReplySync($"Hotloading plugin module '{moduleName}'...");
pluginModule.Start(plugin, true);
info.ReplySync($"Plugin module '{moduleName}' hotloaded successfully.");
});
return Task.FromResult(CommandResult.SUCCESS);
}
public void Start(BasePlugin? plugin) {
if (plugin == null) return;
this.plugin = plugin;
}
}

View File

@@ -0,0 +1,29 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class SetTargetCommand(IServiceProvider provider) : ICommand {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public void Dispose() { }
public void Start() { }
public string Id => "settarget";
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
Server.NextWorldUpdate(() => {
var gamePlayer = converter.GetPlayer(executor);
if (gamePlayer == null) return;
gamePlayer.Pawn.Value?.AcceptInput("AddContext", null, null, "TRAITOR:1");
});
return Task.FromResult(CommandResult.SUCCESS);
}
}

View File

@@ -0,0 +1,43 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class SpecCommand(IServiceProvider provider) : ICommand {
public void Dispose() { }
public void Start() { }
public string Id => "spec";
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
var target = executor;
if (info.ArgCount == 2) {
var finder = provider.GetRequiredService<IPlayerFinder>();
var result = finder.GetPlayerByName(info.Args[1]);
if (result == null) {
info.ReplySync($"Player '{info.Args[1]}' not found.");
return Task.FromResult(CommandResult.ERROR);
}
target = result;
} else if (target == null) {
return Task.FromResult(CommandResult.PLAYER_ONLY);
}
var converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
Server.NextWorldUpdate(() => {
var player = converter.GetPlayer(target);
player?.ChangeTeam(CsTeam.Spectator);
info.ReplySync($"{target.Name} has been moved to Spectators.");
});
return Task.FromResult(CommandResult.SUCCESS);
}
}

View File

@@ -0,0 +1,44 @@
using CounterStrikeSharp.API;
using Microsoft.Extensions.DependencyInjection;
using SpecialRoundAPI;
using TTT.API;
using TTT.API.Command;
using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class SpecialRoundCommand(IServiceProvider provider) : ICommand {
private readonly ISpecialRoundStarter tracker =
provider.GetRequiredService<ISpecialRoundStarter>();
public void Dispose() { }
public void Start() { }
public string Id => "specialround";
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (info.ArgCount == 1) {
tracker.TryStartSpecialRound(null);
info.ReplySync("Started a random special round.");
return Task.FromResult(CommandResult.SUCCESS);
}
var rounds = provider.GetServices<ITerrorModule>()
.OfType<AbstractSpecialRound>()
.ToDictionary(r => r.Name.ToLower(), r => r);
var roundName = info.Args[1].ToLower();
if (!rounds.TryGetValue(roundName, out var round)) {
info.ReplySync($"No special round found with name '{roundName}'.");
foreach (var name in rounds.Keys) info.ReplySync($"- {name}");
return Task.FromResult(CommandResult.INVALID_ARGS);
}
Server.NextWorldUpdate(() => {
tracker.TryStartSpecialRound(round);
info.ReplySync($"Started special round '{roundName}'.");
});
return Task.FromResult(CommandResult.SUCCESS);
}
}

View File

@@ -26,12 +26,19 @@ public class TestCommand(IServiceProvider provider) : ICommand, IPluginModule {
subCommands.Add("sethealth", new SetHealthCommand());
subCommands.Add("emitsound", new EmitSoundCommand(provider));
subCommands.Add("credits", new CreditsCommand(provider));
subCommands.Add("spec", new SpecCommand(provider));
subCommands.Add("reload", new ReloadModuleCommand(provider));
subCommands.Add("specialround", new SpecialRoundCommand(provider));
subCommands.Add("settarget", new SetTargetCommand(provider));
}
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
if (executor.Id != "76561198333588297")
return Task.FromResult(CommandResult.NO_PERMISSION);
if (info.ArgCount == 1) {
foreach (var c in subCommands.Values)
info.ReplySync(

View File

@@ -24,7 +24,7 @@ public class CS2GameConfig : IStorage<TTTConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_ROUND_DURATION_PER_PLAYER = new(
"css_ttt_round_duration_per_player",
"Additional round duration per player in seconds", 30,
"Additional round duration per player in seconds", 15,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 60));
public static readonly FakeConVar<int> CV_ROUND_DURATION_MAX = new(
@@ -63,21 +63,19 @@ public class CS2GameConfig : IStorage<TTTConfig>, IPluginModule {
public static readonly FakeConVar<string> CV_TRAITOR_WEAPONS = new(
"css_ttt_roleweapons_traitor",
"Weapons available to traitors at start of round",
"weapon_knife,weapon_glock", ConVarFlags.FCVAR_NONE,
new ItemValidator(allowMultiple: true));
"Weapons available to traitors at start of round", "",
ConVarFlags.FCVAR_NONE, new ItemValidator(allowMultiple: true));
public static readonly FakeConVar<string> CV_DETECTIVE_WEAPONS = new(
"css_ttt_roleweapons_detective",
"Weapons available to detectives at start of round",
"weapon_knife,weapon_taser,weapon_m4a1,weapon_revolver",
ConVarFlags.FCVAR_NONE, new ItemValidator(allowMultiple: true));
"weapon_taser,weapon_m4a1_silencer,weapon_revolver", ConVarFlags.FCVAR_NONE,
new ItemValidator(allowMultiple: true));
public static readonly FakeConVar<string> CV_INNOCENT_WEAPONS = new(
"css_ttt_roleweapons_innocent",
"Weapons available to innocents at start of round",
"weapon_knife,weapon_glock", ConVarFlags.FCVAR_NONE,
new ItemValidator(allowMultiple: true));
"Weapons available to innocents at start of round", "",
ConVarFlags.FCVAR_NONE, new ItemValidator(allowMultiple: true));
public static readonly FakeConVar<int> CV_TIME_BETWEEN_ROUNDS = new(
"css_ttt_time_between_rounds", "Time to wait between rounds in seconds", 1,

View File

@@ -74,13 +74,13 @@ public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_KARMA_PER_ROUND = new(
"css_ttt_karma_per_round",
"Amount of karma a player will gain at the end of each round", 2,
"Amount of karma a player will gain at the end of each round", 1,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 50));
public static readonly FakeConVar<int> CV_KARMA_PER_ROUND_WIN = new(
"css_ttt_karma_per_round_win",
"Amount of karma a player will gain at the end of each round if their team won",
4, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 50));
1, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 50));
public void Dispose() { }

View File

@@ -10,15 +10,15 @@ namespace TTT.CS2.Configs;
public class CS2ShopConfig : IStorage<ShopConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_STARTING_INNOCENT_CREDITS = new(
"css_ttt_shop_start_innocent", "Starting credits for Innocents", 100,
"css_ttt_shop_start_innocent", "Starting credits for Innocents", 60,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_STARTING_TRAITOR_CREDITS = new(
"css_ttt_shop_start_traitor", "Starting credits for Traitors", 120,
"css_ttt_shop_start_traitor", "Starting credits for Traitors", 100,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_STARTING_DETECTIVE_CREDITS = new(
"css_ttt_shop_start_detective", "Starting credits for Detectives", 150,
"css_ttt_shop_start_detective", "Starting credits for Detectives", 120,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_INNO_V_INNO = new(

View File

@@ -0,0 +1,52 @@
using System.Drawing;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs;
using TTT.API;
using TTT.API.Storage;
namespace TTT.CS2.Configs.ShopItems;
public class CS2BodyPaintConfig : IStorage<BodyPaintConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_bodypaint_price", "Price of the Body Paint item", 30,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_MAX_USES = new(
"css_ttt_shop_bodypaint_max_uses",
"Maximum number of times the Body Paint can be applied per item", 4,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 100));
public static readonly FakeConVar<string> CV_COLOR = new(
"css_ttt_shop_bodypaint_color",
"Color to apply to the player's body (HTML hex or known color name)",
"GreenYellow");
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<BodyPaintConfig?> Load() {
Color parsedColor;
try { parsedColor = ColorTranslator.FromHtml(CV_COLOR.Value); } catch {
try { parsedColor = Color.FromName(CV_COLOR.Value); } catch {
parsedColor = Color.GreenYellow;
}
}
var cfg = new BodyPaintConfig {
Price = CV_PRICE.Value,
MaxUses = CV_MAX_USES.Value,
ColorToApply = parsedColor
};
return Task.FromResult<BodyPaintConfig?>(cfg);
}
}

View File

@@ -0,0 +1,37 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs;
using TTT.API;
using TTT.API.Storage;
namespace TTT.CS2.Configs.ShopItems;
public class CS2CamoConfig : IStorage<CamoConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_camo_price", "Price of the Camo item", 75,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<float> CV_CAMO_VISIBILITY = new(
"css_ttt_shop_camo_visibility",
"Player visibility multiplier while camouflaged (0 = invisible, 1 = fully visible)",
0.5f, ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0f, 1f));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<CamoConfig?> Load() {
var cfg = new CamoConfig {
Price = CV_PRICE.Value, CamoVisibility = CV_CAMO_VISIBILITY.Value
};
return Task.FromResult<CamoConfig?>(cfg);
}
}

View File

@@ -0,0 +1,56 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs.Traitor;
using TTT.API;
using TTT.API.Storage;
namespace TTT.CS2.Configs.ShopItems;
public class CS2ClusterGrenadeConfig : IStorage<ClusterGrenadeConfig>,
IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_clustergrenade_price",
"Price of the Cluster Grenade item (Traitor)", 100, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_GRENADE_COUNT = new(
"css_ttt_shop_clustergrenade_count",
"Number of grenades released upon explosion", 8, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(1, 50));
public static readonly FakeConVar<string> CV_WEAPON_ID = new(
"css_ttt_shop_clustergrenade_weapon",
"Weapon entity ID used for the Cluster Grenade", "weapon_hegrenade");
public static readonly FakeConVar<float> CV_UP_FORCE = new(
"css_ttt_shop_clustergrenade_up_force",
"Upward force applied to cluster fragments", 200f, ConVarFlags.FCVAR_NONE,
new RangeValidator<float>(0f, 1000f));
public static readonly FakeConVar<float> CV_THROW_FORCE = new(
"css_ttt_shop_clustergrenade_throw_force",
"Forward throw force applied to cluster fragments", 250f,
ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0f, 1000f));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<ClusterGrenadeConfig?> Load() {
var cfg = new ClusterGrenadeConfig {
Price = CV_PRICE.Value,
GrenadeCount = CV_GRENADE_COUNT.Value,
UpForce = CV_UP_FORCE.Value,
ThrowForce = CV_THROW_FORCE.Value
};
return Task.FromResult<ClusterGrenadeConfig?>(cfg);
}
}

View File

@@ -0,0 +1,44 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs.Detective;
using TTT.API;
using TTT.API.Storage;
namespace TTT.CS2.Configs.ShopItems;
public class CS2DnaScannerConfig : IStorage<DnaScannerConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_dna_price", "Price of the DNA Scanner item (Detective)", 110,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_MAX_SAMPLES = new(
"css_ttt_shop_dna_max_samples",
"Maximum number of DNA samples that can be stored at once", 0,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 100));
public static readonly FakeConVar<int> CV_DECAY_TIME_SECONDS = new(
"css_ttt_shop_dna_decay_time",
"Time (in seconds) before a DNA sample decays", 120, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(10, 3600));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<DnaScannerConfig?> Load() {
var cfg = new DnaScannerConfig {
Price = CV_PRICE.Value,
MaxSamples = CV_MAX_SAMPLES.Value,
DecayTime = TimeSpan.FromSeconds(CV_DECAY_TIME_SECONDS.Value)
};
return Task.FromResult<DnaScannerConfig?>(cfg);
}
}

View File

@@ -0,0 +1,37 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs.Traitor;
using TTT.API;
using TTT.API.Storage;
namespace TTT.CS2.Configs.ShopItems;
public class CS2GlovesConfig : IStorage<GlovesConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_gloves_price", "Price of the Gloves item (Traitor)", 40,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_MAX_USES = new(
"css_ttt_shop_gloves_max_uses",
"Maximum number of times the Gloves can be used before breaking", 5,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 100));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<GlovesConfig?> Load() {
var cfg = new GlovesConfig {
Price = CV_PRICE.Value, MaxUses = CV_MAX_USES.Value
};
return Task.FromResult<GlovesConfig?>(cfg);
}
}

View File

@@ -0,0 +1,69 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs.Detective;
using TTT.API;
using TTT.API.Storage;
namespace TTT.CS2.Configs.ShopItems;
public class CS2HealthStationConfig : IStorage<HealthStationConfig>,
IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_healthstation_price",
"Price of the Health Station item (Detective)", 50, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<string> CV_USE_SOUND = new(
"css_ttt_shop_healthstation_use_sound",
"Sound played when using the Health Station", "sounds/buttons/blip1");
public static readonly FakeConVar<int> CV_HEALTH_INCREMENTS = new(
"css_ttt_shop_healthstation_increments",
"Number of health increments applied per use", 10, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(1, 100));
public static readonly FakeConVar<int> CV_HEALTH_INTERVAL = new(
"css_ttt_shop_healthstation_interval",
"Interval (in seconds) between health increments", 1,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 60));
public static readonly FakeConVar<int> CV_STATION_HEALTH = new(
"css_ttt_shop_healthstation_station_health",
"Maximum health of the station object itself", 200, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(50, 1000));
public static readonly FakeConVar<int> CV_TOTAL_HEALTH_GIVEN = new(
"css_ttt_shop_healthstation_total_health_given",
"Total health the station can provide before depleting (0 = infinite)", 0,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<float> CV_MAX_RANGE = new(
"css_ttt_shop_healthstation_max_range",
"Maximum range (in units) from which players can use the station", 256f,
ConVarFlags.FCVAR_NONE, new RangeValidator<float>(50f, 2048f));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<HealthStationConfig?> Load() {
var cfg = new HealthStationConfig {
Price = CV_PRICE.Value,
UseSound = CV_USE_SOUND.Value,
HealthIncrements = CV_HEALTH_INCREMENTS.Value,
HealthInterval = TimeSpan.FromSeconds(CV_HEALTH_INTERVAL.Value),
StationHealth = CV_STATION_HEALTH.Value,
TotalHealthGiven = CV_TOTAL_HEALTH_GIVEN.Value,
MaxRange = CV_MAX_RANGE.Value
};
return Task.FromResult<HealthStationConfig?>(cfg);
}
}

View File

@@ -0,0 +1,43 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI;
using TTT.API;
using TTT.API.Storage;
namespace TTT.CS2.Configs.ShopItems;
public class CS2HealthshotConfig : IStorage<HealthshotConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_healthshot_price", "Price of the Healthshot item", 40,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_MAX_PURCHASES = new(
"css_ttt_shop_healthshot_max_purchases",
"Maximum number of times a player can purchase the Healthshot per round", 2,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 100));
public static readonly FakeConVar<string> CV_WEAPON = new(
"css_ttt_shop_healthshot_weapon", "Weapon entity name for the Healthshot",
"weapon_healthshot");
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<HealthshotConfig?> Load() {
var cfg = new HealthshotConfig {
Price = CV_PRICE.Value,
MaxPurchases = CV_MAX_PURCHASES.Value,
Weapon = CV_WEAPON.Value
};
return Task.FromResult<HealthshotConfig?>(cfg);
}
}

View File

@@ -11,7 +11,7 @@ namespace TTT.CS2.Configs.ShopItems;
public class CS2M4A1Config : IStorage<M4A1Config>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_m4a1_price", "Price of the M4A1 item", 75,
"css_ttt_shop_m4a1_price", "Price of the M4A1 item", 50,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<string> CV_CLEAR_SLOTS = new(

View File

@@ -0,0 +1,37 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs.Traitor;
using TTT.API;
using TTT.API.Storage;
namespace TTT.CS2.Configs.ShopItems;
public class CS2OneHitKnifeConfig : IStorage<OneHitKnifeConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_onehitknife_price",
"Price of the One-Hit Knife item (Traitor)", 80, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<bool> CV_FRIENDLY_FIRE = new(
"css_ttt_shop_onehitknife_friendly_fire",
"Whether the One-Hit Knife can damage teammates");
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<OneHitKnifeConfig?> Load() {
var cfg = new OneHitKnifeConfig {
Price = CV_PRICE.Value, FriendlyFire = CV_FRIENDLY_FIRE.Value
};
return Task.FromResult<OneHitKnifeConfig?>(cfg);
}
}

View File

@@ -12,7 +12,7 @@ namespace TTT.CS2.Configs.ShopItems;
public class CS2OneShotDeagleConfig : IStorage<OneShotDeagleConfig>,
IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_onedeagle_price", "Price of the One-Shot Deagle item", 110,
"css_ttt_shop_onedeagle_price", "Price of the One-Shot Deagle item", 130,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<bool> CV_FRIENDLY_FIRE = new(

View File

@@ -0,0 +1,54 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs.Traitor;
using TTT.API;
using TTT.API.Storage;
namespace TTT.CS2.Configs.ShopItems;
public class CS2SilentAWPConfig : IStorage<SilentAWPConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_silentawp_price", "Price of the Silent AWP item (Traitor)",
80, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_WEAPON_INDEX = new(
"css_ttt_shop_silentawp_index", "Weapon slot index for the Silent AWP", 9,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 64));
public static readonly FakeConVar<string> CV_WEAPON_ID = new(
"css_ttt_shop_silentawp_weapon", "Weapon entity ID for the Silent AWP",
"weapon_awp");
public static readonly FakeConVar<int> CV_RESERVE_AMMO = new(
"css_ttt_shop_silentawp_reserve_ammo",
"Reserve ammo count for the Silent AWP", 0, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(0, 100));
public static readonly FakeConVar<int> CV_CURRENT_AMMO = new(
"css_ttt_shop_silentawp_current_ammo",
"Current ammo loaded in the Silent AWP", 1, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(0, 10));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<SilentAWPConfig?> Load() {
var cfg = new SilentAWPConfig {
Price = CV_PRICE.Value,
WeaponIndex = CV_WEAPON_INDEX.Value,
WeaponId = CV_WEAPON_ID.Value,
ReserveAmmo = CV_RESERVE_AMMO.Value,
CurrentAmmo = CV_CURRENT_AMMO.Value
};
return Task.FromResult<SilentAWPConfig?>(cfg);
}
}

View File

@@ -0,0 +1,30 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs.Detective;
using TTT.API;
using TTT.API.Storage;
namespace TTT.CS2.Configs.ShopItems;
public class CS2StickersConfig : IStorage<StickersConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_stickers_price", "Price of the Stickers item (Detective)", 45,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<StickersConfig?> Load() {
var cfg = new StickersConfig { Price = CV_PRICE.Value };
return Task.FromResult<StickersConfig?>(cfg);
}
}

View File

@@ -7,11 +7,11 @@ using TTT.API;
using TTT.API.Storage;
using TTT.CS2.Validators;
namespace TTT.CS2.Configs;
namespace TTT.CS2.Configs.ShopItems;
public class CS2TaserConfig : IStorage<TaserConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_taser_price", "Price of the Taser item", 100,
"css_ttt_shop_taser_price", "Price of the Taser item", 120,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<string> CV_WEAPON = new(

View File

@@ -119,8 +119,11 @@ public static class PlayerExtensions {
SELF(player.Slot), 0.2f, 1);
}
private static RecipientFilter SELF(int slot) => new(slot);
private static RecipientFilter SELF(int slot) {
return new RecipientFilter(slot);
}
private static RecipientFilter OTHERS(int slot)
=> new(ulong.MaxValue & ~(1ul << slot));
private static RecipientFilter OTHERS(int slot) {
return new RecipientFilter(ulong.MaxValue & ~(1ul << slot));
}
}

View File

@@ -10,6 +10,7 @@ using TTT.CS2.Roles;
using TTT.CS2.Utils;
using TTT.Game;
using TTT.Game.Events.Game;
using TTT.Game.lang;
using TTT.Game.Roles;
namespace TTT.CS2.Game;

View File

@@ -6,18 +6,17 @@ using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.CS2.Extensions;
using TTT.Game.Events.Body;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
using TTT.Game.Roles;
namespace TTT.CS2.GameHandlers;
public class BodySpawner(IServiceProvider provider) : BaseListener(provider) {
public class BodySpawner(IServiceProvider provider) : IPluginModule {
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly IPlayerConverter<CCSPlayerController> converter =
@@ -26,39 +25,31 @@ public class BodySpawner(IServiceProvider provider) : BaseListener(provider) {
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
[UsedImplicitly]
[EventHandler]
public void OnLeave(PlayerLeaveEvent ev) {
if (games.ActiveGame is not { State: State.IN_PROGRESS }) return;
spawnRagdoll(ev.Player);
}
public void Dispose() { }
public void Start() { }
[UsedImplicitly]
[EventHandler]
public void OnDeath(PlayerDeathEvent ev) {
if (games.ActiveGame is not { State: State.IN_PROGRESS }) return;
spawnRagdoll(ev.Player, ev.Killer, ev.Weapon);
}
private void spawnRagdoll(IPlayer apiPlayer, IOnlinePlayer? killer = null,
string? weapon = null) {
var player = converter.GetPlayer(apiPlayer);
if (player == null || !player.IsValid) return;
[GameEventHandler]
public HookResult OnDeath(EventPlayerDeath ev, GameEventInfo _) {
if (games.ActiveGame is not { State: State.IN_PROGRESS })
return HookResult.Continue;
var player = ev.Userid;
if (player == null || !player.IsValid) return HookResult.Continue;
player.SetColor(Color.FromArgb(0, 255, 255, 255));
var ragdollBody = makeGameRagdoll(player);
var body = new CS2Body(ragdollBody, converter.GetPlayer(player));
if (killer != null) body.WithKiller(killer);
if (ev.Attacker != null && ev.Attacker.IsValid)
body.WithKiller(converter.GetPlayer(ev.Attacker));
body.WithWeapon(new BaseWeapon(weapon ?? "unknown"));
body.WithWeapon(new BaseWeapon(ev.Weapon));
var bodyCreatedEvent = new BodyCreateEvent(body);
bus.Dispatch(bodyCreatedEvent);
if (bodyCreatedEvent.IsCanceled) {
ragdollBody.AcceptInput("Kill");
}
if (bodyCreatedEvent.IsCanceled) ragdollBody.AcceptInput("Kill");
return HookResult.Continue;
}
[UsedImplicitly]

View File

@@ -29,9 +29,9 @@ public class BuyMenuHandler(IServiceProvider provider) : IPluginModule {
{ "weapon_usp_silencer", "M4A1" },
{ "weapon_sg556", "M4A1" },
{ "weapon_mp5sd", "M4A1" },
{ "weapon_decoy", "healthshot" },
{ "weapon_awp", "AWP" },
{ "weapon_hegrenade", "Cluster" }
{ "weapon_hegrenade", "Cluster" },
{ "weapon_decoy", "Teleport Decoy" }
};
public void Dispose() { }
@@ -49,6 +49,14 @@ public class BuyMenuHandler(IServiceProvider provider) : IPluginModule {
}
inventory.RemoveWeapon(player, new BaseWeapon(ev.Weapon));
switch (ev.Weapon) {
case "weapon_m4a1_silencer":
inventory.RemoveWeaponInSlot(player, 0);
break;
case "weapon_revolver":
inventory.RemoveWeaponInSlot(player, 1);
break;
}
if (!shopAliases.TryGetValue(ev.Weapon, out var alias))
return HookResult.Continue;

View File

@@ -7,8 +7,10 @@ using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.CS2.API;
using TTT.Game.Events.Player;
using TTT.Game.Roles;
namespace TTT.CS2.GameHandlers;
@@ -24,6 +26,9 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
private readonly IAliveSpoofer spoofer =
provider.GetRequiredService<IAliveSpoofer>();
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
public void Start() { }
public void Dispose() { }
@@ -45,12 +50,36 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
if (games.ActiveGame is not { State: State.IN_PROGRESS })
return HookResult.Continue;
if (ev.Attacker != null) {
ev.FireEventToClient(ev.Attacker);
var apiPlayer = converter.GetPlayer(ev.Attacker);
var role = roles.GetRoles(apiPlayer);
if (role.Any(r => r is TraitorRole))
foreach (var p in Utilities.GetPlayers()) {
var apiP = converter.GetPlayer(p);
if (apiP.Id == apiPlayer.Id) continue;
var r = roles.GetRoles(converter.GetPlayer(p));
if (role.Intersect(r).Any()) ev.FireEventToClient(p);
}
}
info.DontBroadcast = true;
spoofer.SpoofAlive(player);
Server.NextWorldUpdateAsync(() => bus.Dispatch(deathEvent));
return HookResult.Continue;
}
[UsedImplicitly]
[GameEventHandler(HookMode.Pre)]
public HookResult OnPlayerDamage(EventPlayerHurt ev, GameEventInfo info) {
var player = ev.Userid;
if (player == null) return HookResult.Continue;
hideAndTrackStats(ev);
return HookResult.Continue;
}
private void hideAndTrackStats(EventPlayerDeath ev,
CCSPlayerController player) {
var victimStats = player.ActionTrackingServices?.MatchStats;
@@ -67,13 +96,16 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
"CCSPlayerController_ActionTrackingServices",
"m_pActionTrackingServices");
if (killerStats == null) return;
killerStats.Kills -= 1;
killerStats.Damage -= ev.DmgHealth;
killerStats.Kills -= 1;
killerStats.Damage -= ev.DmgHealth;
killerStats.UtilityDamage = 0;
if (ev.Attacker.ActionTrackingServices != null)
ev.Attacker.ActionTrackingServices.NumRoundKills--;
Utilities.SetStateChanged(ev.Attacker, "CSPerRoundStats_t", "m_iDamage");
Utilities.SetStateChanged(ev.Attacker, "CSPerRoundStats_t",
"m_iUtilityDamage");
Utilities.SetStateChanged(ev.Attacker, "CCSPlayerController",
"m_pActionTrackingServices");
ev.FireEventToClient(ev.Attacker);
}
var assisterStats = ev.Assister?.ActionTrackingServices?.MatchStats;
@@ -85,6 +117,16 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
"m_pActionTrackingServices");
}
private void hideAndTrackStats(EventPlayerHurt ev) {
var attackerStats = ev.Attacker?.ActionTrackingServices?.MatchStats;
if (attackerStats == null) return;
if (ev.Attacker == null) return;
attackerStats.Damage -= ev.DmgHealth;
Utilities.SetStateChanged(ev.Attacker, "CCSPlayerController",
"m_pActionTrackingServices");
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnPlayerHurt(EventPlayerHurt ev, GameEventInfo _) {

View File

@@ -30,6 +30,10 @@ public class DamageCanceler(IServiceProvider provider) : IPluginModule {
}
private HookResult onTakeDamage(DynamicHook hook) {
var playerPawn = hook.GetParam<CCSPlayerPawn>(0);
var player = playerPawn.Controller.Value?.As<CCSPlayerController>();
if (player == null || !player.IsValid) return HookResult.Continue;
var damagedEvent = new PlayerDamagedEvent(converter, hook);
bus.Dispatch(damagedEvent);

View File

@@ -1,5 +1,7 @@
using TTT.API.Events;
using JetBrains.Annotations;
using TTT.API.Events;
using TTT.API.Game;
using TTT.CS2.Actions;
using TTT.CS2.lang;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
@@ -8,6 +10,7 @@ namespace TTT.CS2.GameHandlers.DamageCancelers;
public class TaserListenCanceler(IServiceProvider provider)
: BaseListener(provider) {
[UsedImplicitly]
[EventHandler]
public void OnHurt(PlayerDamagedEvent ev) {
if (Games.ActiveGame is not { State: State.IN_PROGRESS }) return;
@@ -23,5 +26,6 @@ public class TaserListenCanceler(IServiceProvider provider)
Messenger.Message(attacker,
Locale[CS2Msgs.TASER_SCANNED(victim, Roles.GetRoles(victim).First())]);
Games.ActiveGame.Logger.LogAction(new TaserAction(Roles, victim, attacker));
}
}

View File

@@ -1,25 +0,0 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.UserMessages;
using TTT.API;
namespace TTT.CS2.GameHandlers;
public class DebugMessage : IPluginModule {
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
for (var i = 0; i < 10000; i++) {
if (i is 325 or 124) continue;
var j = i;
plugin.HookUserMessage(i, msg => debug(msg, j));
}
}
private HookResult debug(UserMessage native, int id) {
Server.PrintToConsole(id + "");
Server.PrintToConsole(native.DebugString);
return HookResult.Continue;
}
}

View File

@@ -1,19 +1,24 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.CS2.Extensions;
using TTT.Game.Events.Game;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
namespace TTT.CS2.Listeners;
namespace TTT.CS2.GameHandlers;
public class LateSpawnListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
[UsedImplicitly]
[EventHandler]
public void OnJoin(PlayerJoinEvent ev) {
if (Games.ActiveGame is { State: State.IN_PROGRESS }) return;
@@ -24,4 +29,17 @@ public class LateSpawnListener(IServiceProvider provider)
player.Respawn();
});
}
[UsedImplicitly]
[EventHandler]
public void GameState(GameStateUpdateEvent ev) {
if (ev.NewState is State.FINISHED or State.WAITING) return;
Server.NextWorldUpdate(() => {
foreach (var player in Utilities.GetPlayers()
.Where(p => p.GetHealth() <= 0 && p.Team != CsTeam.Spectator
&& p.Team != CsTeam.None))
player.Respawn();
});
}
}

View File

@@ -0,0 +1,26 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Game;
namespace TTT.CS2.GameHandlers;
public class MapChangeCausesEndListener(IServiceProvider provider)
: IPluginModule {
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
plugin?.RegisterListener<CounterStrikeSharp.API.Core.Listeners.OnMapStart>(
onMapChange);
}
private void onMapChange(string mapName) {
games.ActiveGame?.EndGame(new EndReason("Map Change"));
Server.PrintToConsole("Detected map change, ending active game.");
}
}

View File

@@ -0,0 +1,22 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Player;
using TTT.Game.Events.Game;
using TTT.Game.Listeners;
namespace TTT.CS2.GameHandlers;
public class NameUpdater(IServiceProvider provider) : BaseListener(provider) {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
[UsedImplicitly]
[EventHandler]
public void OnGameInit(GameInitEvent ev) {
foreach (var player in Utilities.GetPlayers())
converter.GetPlayer(player).Name = player.PlayerName;
}
}

View File

@@ -4,9 +4,12 @@ using CounterStrikeSharp.API.Core.Attributes.Registration;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.CS2.lang;
using TTT.Game.Events.Game;
using TTT.Locale;
namespace TTT.CS2.GameHandlers;
@@ -15,6 +18,9 @@ public class PlayerMuter(IServiceProvider provider) : IPluginModule {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IGameManager game =
provider.GetRequiredService<IGameManager>();
private readonly IMsgLocalizer locale =
provider.GetRequiredService<IMsgLocalizer>();
@@ -36,6 +42,8 @@ public class PlayerMuter(IServiceProvider provider) : IPluginModule {
if (player.Pawn.Value is { Health: > 0 }) return;
if (game.ActiveGame is not { State: State.IN_PROGRESS }) return;
if ((player.VoiceFlags & VoiceFlags.Muted) != VoiceFlags.Muted) {
var apiPlayer = converter.GetPlayer(player);
messenger.Message(apiPlayer, locale[CS2Msgs.DEAD_MUTE_REMINDER]);
@@ -52,4 +60,12 @@ public class PlayerMuter(IServiceProvider provider) : IPluginModule {
player.VoiceFlags &= ~VoiceFlags.Muted;
return HookResult.Continue;
}
[UsedImplicitly]
[EventHandler]
public void OnGameEvent(GameStateUpdateEvent ev) {
if (ev.NewState != State.FINISHED) return;
foreach (var p in Utilities.GetPlayers()) p.VoiceFlags &= ~VoiceFlags.Muted;
}
}

View File

@@ -51,7 +51,9 @@ public class PropMover(IServiceProvider provider) : IPluginModule {
return;
}
if (!pressed.HasFlag(PlayerButtons.Use)) return;
if (!pressed.HasFlag(PlayerButtons.Use)
&& !pressed.HasFlag(PlayerButtons.Inspect))
return;
onStartUse(player);
}
@@ -90,9 +92,13 @@ public class PropMover(IServiceProvider provider) : IPluginModule {
if (!released.HasFlag(PlayerButtons.Use)) return;
playersPressingE.Remove(player);
if (!heldItem.Ragdoll.IsValid) return;
heldItem.Ragdoll.AcceptInput("EnableMotion");
if (heldItem.Beam != null && heldItem.Beam.IsValid)
heldItem.Beam.AcceptInput("Kill");
// Check if any other players are still holding this ragdoll
foreach (var (_, info) in playersPressingE)
if (info.Ragdoll == heldItem.Ragdoll)
return;
heldItem.Ragdoll.AcceptInput("EnableMotion");
}
private void refreshHeld() {

View File

@@ -6,7 +6,6 @@ using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.CS2.Extensions;
@@ -75,6 +74,12 @@ public class RoleIconsHandler(IServiceProvider provider)
SetVisiblePlayers(gamePlayer.Slot, playersBitmask);
}
public void RevealToAll(IOnlinePlayer online) {
var gamePlayer = players.GetPlayer(online);
if (gamePlayer == null || !gamePlayer.IsValid) return;
RevealToAll(gamePlayer.Slot);
}
public void ClearAllVisibility() {
Array.Clear(visibilities, 0, visibilities.Length);
}
@@ -98,8 +103,7 @@ public class RoleIconsHandler(IServiceProvider provider)
[UsedImplicitly]
[EventHandler(IgnoreCanceled = true)]
public void OnRoundStart(GameStateUpdateEvent ev) {
if (ev.NewState != State.FINISHED) return;
public void OnRoundStart(GameInitEvent ev) {
for (var i = 0; i < icons.Length; i++) removeIcon(i);
ClearAllVisibility();
traitorsThisRound.Clear();

View File

@@ -12,16 +12,18 @@ namespace TTT.CS2.GameHandlers;
public class RoundStart_GameStartHandler(IServiceProvider provider)
: IPluginModule {
private readonly TTTConfig config =
provider.GetService<IStorage<TTTConfig>>()?.Load().GetAwaiter().GetResult()
?? new TTTConfig();
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
private TTTConfig config
=> provider.GetService<IStorage<TTTConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new TTTConfig();
public void Dispose() { }
public void Start() { }

View File

@@ -9,11 +9,16 @@ using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.CS2.API;
using TTT.CS2.Extensions;
using TTT.Game.Events.Player;
namespace TTT.CS2.GameHandlers;
public class TeamChangeHandler(IServiceProvider provider) : IPluginModule {
private readonly IBodyTracker bodies =
provider.GetRequiredService<IBodyTracker>();
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly IPlayerConverter<CCSPlayerController> converter =
@@ -33,6 +38,8 @@ public class TeamChangeHandler(IServiceProvider provider) : IPluginModule {
CommandInfo commandInfo) {
CsTeam requestedTeam;
if (player == null) return HookResult.Continue;
if (int.TryParse(commandInfo.GetArg(1), out var teamIndex))
requestedTeam = (CsTeam)teamIndex;
else
@@ -44,15 +51,21 @@ public class TeamChangeHandler(IServiceProvider provider) : IPluginModule {
};
if (games.ActiveGame is not { State: State.IN_PROGRESS }) {
if (player != null && player.LifeState != (int)LifeState_t.LIFE_ALIVE)
Server.NextWorldUpdate(player.Respawn);
if (player.GetHealth() <= 0) Server.NextWorldUpdate(player.Respawn);
return HookResult.Continue;
}
if (requestedTeam is CsTeam.CounterTerrorist or CsTeam.Terrorist)
if (player != null && player.Team is CsTeam.Spectator or CsTeam.None)
if (player.Team is CsTeam.Spectator or CsTeam.None)
return HookResult.Continue;
var apiPlayer = converter.GetPlayer(player);
// If the player is dead and already identified, let them move to spec
if (bodies.Bodies.Keys.Any(b
=> b.OfPlayer.Id == apiPlayer.Id && b.IsIdentified))
return HookResult.Continue;
return HookResult.Handled;
}

View File

@@ -1,5 +1,6 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Utils;
using MAULActainShared.plugin;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
@@ -7,6 +8,7 @@ using TTT.API.Game;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.API.Role;
using TTT.CS2.Extensions;
using TTT.CS2.lang;
using TTT.CS2.ThirdParties.eGO;
using TTT.Game.Roles;
@@ -27,6 +29,8 @@ public class TraitorChatHandler(IServiceProvider provider) : IPluginModule {
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
private readonly IMuted? mutedPlayers = provider.GetService<IMuted>();
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
@@ -36,37 +40,57 @@ public class TraitorChatHandler(IServiceProvider provider) : IPluginModule {
try {
maulService ??= EgoApi.MAUL.Get();
if (maulService != null) {
maulService.getChatShareService().OnChatShare += OnOnChatShare;
maulService.getChatShareService().OnChatShare += OnChatShare;
return;
}
plugin?.AddCommandListener("say_team", onSay);
plugin?.AddCommandListener("say", onSay);
} catch (KeyNotFoundException) {
plugin?.AddCommandListener("say_team", onSay);
plugin?.AddCommandListener("say", onSay);
}
}
public void Dispose() {
if (maulService != null)
maulService.getChatShareService().OnChatShare -= OnOnChatShare;
maulService.getChatShareService().OnChatShare -= OnChatShare;
}
public void Start() { }
private void OnOnChatShare(CCSPlayerController? player, CommandInfo info,
private void OnChatShare(CCSPlayerController? player, CommandInfo info,
ref bool canceled) {
if (player == null) return;
if (mutedPlayers != null
&& mutedPlayers.Contains(player.SteamID.ToString())) {
canceled = true;
return;
}
if (!info.GetArg(0).Equals("say_team", StringComparison.OrdinalIgnoreCase))
return;
var result = onSay(player, info);
if (result == HookResult.Handled) canceled = true;
if (player.Team == CsTeam.CounterTerrorist) return;
var result = onSay(player, info);
canceled = true;
if (result == HookResult.Handled) return;
player?.ExecuteClientCommandFromServer("say " + info.ArgString);
}
private HookResult onSay(CCSPlayerController? player,
CommandInfo commandInfo) {
if (mutedPlayers != null && player != null && player.GetHealth() > 0
&& mutedPlayers.Contains(player?.SteamID.ToString() ?? ""))
return HookResult.Handled;
if (commandInfo.GetArg(0).Equals("say", StringComparison.OrdinalIgnoreCase))
return HookResult.Continue;
if (player == null
|| game.ActiveGame is not { State: State.IN_PROGRESS or State.FINISHED }
|| converter.GetPlayer(player) is not IOnlinePlayer apiPlayer
|| !roles.GetRoles(apiPlayer).Any(r => r is TraitorRole))
|| !roles.GetRoles(apiPlayer).Any(r => r is TraitorRole)
|| player.GetHealth() <= 0)
return HookResult.Continue;
var teammates = game.ActiveGame?.Players.Where(p

View File

@@ -16,15 +16,15 @@ public static class ArmorItemServicesCollection {
}
public class ArmorItem(IServiceProvider provider) : BaseItem(provider) {
private readonly ArmorConfig config = provider
.GetService<IStorage<ArmorConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new ArmorConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private ArmorConfig config
=> Provider.GetService<IStorage<ArmorConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new ArmorConfig();
public override string Name => Locale[ArmorMsgs.SHOP_ITEM_ARMOR];
public override string Description => Locale[ArmorMsgs.SHOP_ITEM_ARMOR_DESC];
public override ShopItemConfig Config => config;

View File

@@ -17,11 +17,11 @@ public static class BodyPaintServicesCollection {
public class BodyPaintItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider) {
private readonly BodyPaintConfig config = provider
.GetService<IStorage<BodyPaintConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new BodyPaintConfig();
private BodyPaintConfig config
=> Provider.GetService<IStorage<BodyPaintConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new BodyPaintConfig();
public override string Name => Locale[BodyPaintMsgs.SHOP_ITEM_BODY_PAINT];

View File

@@ -17,16 +17,16 @@ public class BodyPaintListener(IServiceProvider provider)
private readonly IBodyTracker bodies =
provider.GetRequiredService<IBodyTracker>();
private readonly BodyPaintConfig config =
provider.GetService<IStorage<BodyPaintConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new BodyPaintConfig();
private readonly IShop shop = provider.GetRequiredService<IShop>();
private readonly Dictionary<IPlayer, int> uses = new();
private BodyPaintConfig config
=> Provider.GetService<IStorage<BodyPaintConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new BodyPaintConfig();
[UsedImplicitly]
[EventHandler(Priority = Priority.HIGH)]
public void BodyIdentify(BodyIdentifyEvent ev) {

View File

@@ -18,13 +18,15 @@ public static class CamoServiceCollection {
}
public class CamouflageItem(IServiceProvider provider) : BaseItem(provider) {
private readonly CamoConfig config =
provider.GetService<IStorage<CamoConfig>>()?.Load().GetAwaiter().GetResult()
?? new CamoConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private CamoConfig config
=> Provider.GetService<IStorage<CamoConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new CamoConfig();
public override string Name => Locale[CamoMsgs.SHOP_ITEM_CAMO];
public override string Description => Locale[CamoMsgs.SHOP_ITEM_CAMO_DESC];
public override ShopItemConfig Config => config;

View File

@@ -18,11 +18,11 @@ public static class ClusterGrenadeServiceCollection {
public class ClusterGrenadeItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider) {
private readonly ClusterGrenadeConfig config = provider
.GetService<IStorage<ClusterGrenadeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new ClusterGrenadeConfig();
private ClusterGrenadeConfig config
=> Provider.GetService<IStorage<ClusterGrenadeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new ClusterGrenadeConfig();
public override string Name
=> Locale[ClusterGrenadeMsgs.SHOP_ITEM_CLUSTER_GRENADE];

View File

@@ -1,9 +1,6 @@
using System.Reactive.Concurrency;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
@@ -11,23 +8,26 @@ using ShopAPI;
using ShopAPI.Configs.Traitor;
using TTT.API;
using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
using TTT.CS2.Utils;
namespace TTT.CS2.Items.ClusterGrenade;
public class ClusterGrenadeListener(IServiceProvider provider) : IPluginModule {
private readonly ClusterGrenadeConfig config =
provider.GetService<IStorage<ClusterGrenadeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new ClusterGrenadeConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IShop shop = provider.GetRequiredService<IShop>();
private ClusterGrenadeConfig config
=> provider.GetService<IStorage<ClusterGrenadeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new ClusterGrenadeConfig();
public void Dispose() { }
public void Start() { }
[UsedImplicitly]
[GameEventHandler]
public HookResult OnHeGrenade(EventHegrenadeDetonate ev, GameEventInfo _) {
@@ -59,7 +59,4 @@ public class ClusterGrenadeListener(IServiceProvider provider) : IPluginModule {
return HookResult.Continue;
}
public void Dispose() { }
public void Start() { }
}

View File

@@ -1,5 +1,3 @@
using System.Linq;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Timers;
using CounterStrikeSharp.API.Modules.Utils;
@@ -10,7 +8,6 @@ using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
using TTT.API;
using TTT.API.Events;
using TTT.API.Extensions;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
@@ -18,22 +15,20 @@ using TTT.API.Storage;
using TTT.CS2.Extensions;
using TTT.CS2.Utils;
using TTT.Game.Events.Game;
using TTT.Game.Roles;
namespace TTT.CS2.Items.Compass;
/// <summary>
/// Base compass that renders a heading toward the nearest target returned by GetTargets.
/// Child classes decide which targets to expose and who owns the item.
/// Base compass that renders a heading toward the nearest target returned by GetTargets.
/// Child classes decide which targets to expose and who owns the item.
/// </summary>
public abstract class AbstractCompassItem<TRole> : RoleRestrictedItem<TRole>,
IListener, IPluginModule where TRole : class, IRole {
protected readonly CompassConfig config;
protected readonly IPlayerConverter<CCSPlayerController> Converter;
protected readonly ISet<IPlayer> Owners = new HashSet<IPlayer>();
protected AbstractCompassItem(IServiceProvider provider) : base(provider) {
config = provider.GetService<IStorage<CompassConfig>>()
_Config = provider.GetService<IStorage<CompassConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new CompassConfig();
@@ -42,7 +37,9 @@ public abstract class AbstractCompassItem<TRole> : RoleRestrictedItem<TRole>,
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
}
public override ShopItemConfig Config => config;
protected CompassConfig _Config { get; }
public override ShopItemConfig Config => _Config;
public void Start(BasePlugin? plugin) {
base.Start();
@@ -56,14 +53,14 @@ public abstract class AbstractCompassItem<TRole> : RoleRestrictedItem<TRole>,
}
/// <summary>
/// Return world positions to point at for this player.
/// Return world positions to point at for this player.
/// </summary>
protected abstract IList<Vector> GetTargets(IOnlinePlayer requester);
abstract protected IList<Vector> GetTargets(IOnlinePlayer requester);
/// <summary>
/// Whether this player currently owns/has this compass effect.
/// Whether this player currently owns/has this compass effect.
/// </summary>
protected abstract bool OwnsItem(IOnlinePlayer player);
abstract protected bool OwnsItem(IOnlinePlayer player);
public override void OnPurchase(IOnlinePlayer player) { Owners.Add(player); }
@@ -80,6 +77,7 @@ public abstract class AbstractCompassItem<TRole> : RoleRestrictedItem<TRole>,
foreach (var player in Owners.OfType<IOnlinePlayer>()) {
var gamePlayer = Converter.GetPlayer(player);
if (gamePlayer == null) continue;
if (!player.IsAlive) continue;
ShowCompass(gamePlayer, player);
}
}
@@ -95,7 +93,7 @@ public abstract class AbstractCompassItem<TRole> : RoleRestrictedItem<TRole>,
if (targets.Count == 0) return;
var (nearest, distance) = GetNearestVector(src, targets);
if (nearest == null || distance > config.MaxRange) return;
if (nearest == null || distance > _Config.MaxRange) return;
var normalizedYaw = AdjustGameAngle(viewer.PlayerPawn.Value.EyeAngles.Y);
@@ -120,8 +118,8 @@ public abstract class AbstractCompassItem<TRole> : RoleRestrictedItem<TRole>,
}
private string GenerateCompass(float pointing, float target) {
return TextCompass.GenerateCompass(config.CompassFOV, config.CompassLength,
pointing, targetDir: target);
return TextCompass.GenerateCompass(_Config.CompassFOV,
_Config.CompassLength, pointing, targetDir: target);
}
private static string GetDistanceDescription(float distance) {

View File

@@ -27,11 +27,11 @@ public class BodyCompassItem(IServiceProvider provider)
=> Locale[CompassMsgs.SHOP_ITEM_COMPASS_BODY_DESC];
/// <summary>
/// For innocents: point to nearest traitor.
/// For traitors: point to nearest non-traitor (ally list in original code).
/// Returns target world positions as vectors.
/// For innocents: point to nearest traitor.
/// For traitors: point to nearest non-traitor (ally list in original code).
/// Returns target world positions as vectors.
/// </summary>
protected override IList<Vector> GetTargets(IOnlinePlayer requester) {
override protected IList<Vector> GetTargets(IOnlinePlayer requester) {
if (Games.ActiveGame is not { State: State.IN_PROGRESS or State.FINISHED })
return Array.Empty<Vector>();

View File

@@ -23,11 +23,11 @@ public class InnoCompassItem(IServiceProvider provider)
=> Locale[CompassMsgs.SHOP_ITEM_COMPASS_PLAYER_DESC];
/// <summary>
/// For innocents: point to nearest traitor.
/// For traitors: point to nearest non-traitor (ally list in original code).
/// Returns target world positions as vectors.
/// For innocents: point to nearest traitor.
/// For traitors: point to nearest non-traitor (ally list in original code).
/// Returns target world positions as vectors.
/// </summary>
protected override IList<Vector> GetTargets(IOnlinePlayer requester) {
override protected IList<Vector> GetTargets(IOnlinePlayer requester) {
if (Games.ActiveGame is not { State: State.IN_PROGRESS or State.FINISHED })
return Array.Empty<Vector>();

View File

@@ -28,15 +28,15 @@ public class DnaListener(IServiceProvider provider) : BaseListener(provider) {
private readonly IBodyTracker bodies =
provider.GetRequiredService<IBodyTracker>();
private readonly DnaScannerConfig config = provider
.GetService<IStorage<DnaScannerConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new DnaScannerConfig();
private readonly Dictionary<string, DateTime> lastMessages = new();
private readonly IShop shop = provider.GetRequiredService<IShop>();
private DnaScannerConfig config
=> Provider.GetService<IStorage<DnaScannerConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new DnaScannerConfig();
// Low priority to allow body identification to happen first
[UsedImplicitly]
[EventHandler(Priority = Priority.LOW)]

View File

@@ -1,6 +1,6 @@
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.CS2.Items.DNA;

View File

@@ -18,11 +18,11 @@ public static class DnaScannerServiceCollection {
public class DnaScanner(IServiceProvider provider)
: RoleRestrictedItem<DetectiveRole>(provider) {
private readonly DnaScannerConfig config = provider
.GetService<IStorage<DnaScannerConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new DnaScannerConfig();
private DnaScannerConfig config
=> Provider.GetService<IStorage<DnaScannerConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new DnaScannerConfig();
public override string Name => Locale[DnaMsgs.SHOP_ITEM_DNA];
public override string Description => Locale[DnaMsgs.SHOP_ITEM_DNA_DESC];

View File

@@ -18,11 +18,11 @@ public static class OneHitKnifeServiceCollection {
public class OneHitKnife(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider) {
private readonly OneHitKnifeConfig config = provider
.GetService<IStorage<OneHitKnifeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new OneHitKnifeConfig();
private OneHitKnifeConfig config
=> Provider.GetService<IStorage<OneHitKnifeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new OneHitKnifeConfig();
public override string Name
=> Locale[OneHitKnifeMsgs.SHOP_ITEM_ONE_HIT_KNIFE];

View File

@@ -4,7 +4,6 @@ using ShopAPI;
using ShopAPI.Configs.Traitor;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
@@ -13,14 +12,14 @@ namespace TTT.CS2.Items.OneHitKnife;
public class OneHitKnifeListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly OneHitKnifeConfig config =
provider.GetService<IStorage<OneHitKnifeConfig>>()
private readonly IShop shop = provider.GetRequiredService<IShop>();
private OneHitKnifeConfig config
=> Provider.GetService<IStorage<OneHitKnifeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new OneHitKnifeConfig();
private readonly IShop shop = provider.GetRequiredService<IShop>();
[UsedImplicitly]
[EventHandler]
public void OnDamage(PlayerDamagedEvent ev) {

View File

@@ -10,7 +10,7 @@ using TTT.Game.Roles;
namespace TTT.CS2.Items.PoisonShots;
public static class PoisonShotServiceCollection {
public static void AddPoisonShots(this IServiceCollection services) {
public static void AddPoisonShotsServices(this IServiceCollection services) {
services.AddModBehavior<PoisonShotsItem>();
services.AddModBehavior<PoisonShotsListener>();
}
@@ -18,11 +18,11 @@ public static class PoisonShotServiceCollection {
public class PoisonShotsItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider) {
private readonly PoisonShotsConfig config = provider
.GetService<IStorage<PoisonShotsConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new PoisonShotsConfig();
private PoisonShotsConfig config
=> Provider.GetService<IStorage<PoisonShotsConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new PoisonShotsConfig();
public override string Name => Locale[PoisonShotMsgs.SHOP_ITEM_POISON_SHOTS];

View File

@@ -24,15 +24,11 @@ public class PoisonShotsListener(IServiceProvider provider)
: BaseListener(provider), IPluginModule {
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly PoisonShotsConfig config =
provider.GetService<IStorage<PoisonShotsConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new PoisonShotsConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly Dictionary<string, IPlayer> killedWithPoison = new();
private readonly Dictionary<IPlayer, int> poisonShots = new();
private readonly List<IDisposable> poisonTimers = [];
@@ -42,7 +38,11 @@ public class PoisonShotsListener(IServiceProvider provider)
private readonly IShop shop = provider.GetRequiredService<IShop>();
private readonly ISet<string> killedWithPoison = new HashSet<string>();
private PoisonShotsConfig config
=> Provider.GetService<IStorage<PoisonShotsConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new PoisonShotsConfig();
public override void Dispose() {
base.Dispose();
@@ -118,7 +118,7 @@ public class PoisonShotsListener(IServiceProvider provider)
if (dmgEvent.IsCanceled) return true;
if (online.Health - config.PoisonConfig.DamagePerTick <= 0) {
killedWithPoison.Add(online.Id);
killedWithPoison[online.Id] = effect.Shooter;
var deathEvent = new PlayerDeathEvent(online)
.WithKiller(effect.Shooter as IOnlinePlayer)
.WithWeapon($"[{Locale[PoisonShotMsgs.SHOP_ITEM_POISON_SHOTS]}]");
@@ -156,19 +156,21 @@ public class PoisonShotsListener(IServiceProvider provider)
return 0;
}
[UsedImplicitly]
[EventHandler]
public void OnRagdollSpawn(BodyCreateEvent ev) {
if (!killedWithPoison.TryGetValue(ev.Body.OfPlayer.Id, out var shooter))
return;
if (ev.Body.Killer != null && ev.Body.Killer.Id != ev.Body.OfPlayer.Id)
return;
ev.Body.Killer = shooter as IOnlinePlayer;
}
private class PoisonEffect(IPlayer player, IPlayer shooter) {
public IPlayer Player { get; } = player;
public IPlayer Shooter { get; } = shooter;
public int Ticks { get; set; }
public int DamageGiven { get; set; }
}
[UsedImplicitly]
[EventHandler]
public void OnRagdollSpawn(BodyCreateEvent ev) {
if (!killedWithPoison.Contains(ev.Body.OfPlayer.Id)) return;
if (ev.Body.Killer == null || ev.Body.Killer.Id == ev.Body.OfPlayer.Id)
ev.IsCanceled = true;
}
}

View File

@@ -10,7 +10,7 @@ using TTT.Game.Roles;
namespace TTT.CS2.Items.PoisonSmoke;
public static class PoisonSmokeServiceCollection {
public static void AddPoisonSmoke(this IServiceCollection services) {
public static void AddPoisonSmokeServices(this IServiceCollection services) {
services.AddModBehavior<PoisonSmokeItem>();
services.AddModBehavior<PoisonSmokeListener>();
}
@@ -18,8 +18,8 @@ public static class PoisonSmokeServiceCollection {
public class PoisonSmokeItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider) {
private readonly PoisonSmokeConfig config =
provider.GetService<IStorage<PoisonSmokeConfig>>()
private PoisonSmokeConfig config
=> Provider.GetService<IStorage<PoisonSmokeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new PoisonSmokeConfig();

View File

@@ -12,7 +12,6 @@ using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
using TTT.CS2.Extensions;
using TTT.Game.Events.Body;
@@ -25,20 +24,20 @@ namespace TTT.CS2.Items.PoisonSmoke;
public class PoisonSmokeListener(IServiceProvider provider)
: BaseListener(provider), IPluginModule {
private readonly PoisonSmokeConfig config =
provider.GetService<IStorage<PoisonSmokeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new PoisonSmokeConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly ISet<string> killedWithPoison = new HashSet<string>();
private readonly List<IDisposable> poisonSmokes = [];
private readonly IShop shop = provider.GetRequiredService<IShop>();
private readonly ISet<string> killedWithPoison = new HashSet<string>();
private PoisonSmokeConfig config
=> Provider.GetService<IStorage<PoisonSmokeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new PoisonSmokeConfig();
public override void Dispose() {
base.Dispose();
@@ -129,15 +128,6 @@ public class PoisonSmokeListener(IServiceProvider provider)
return effect.DamageGiven < config.PoisonConfig.TotalDamage;
}
private class PoisonEffect(CSmokeGrenadeProjectile projectile,
IOnlinePlayer attacker) {
public int Ticks { get; set; }
public int DamageGiven { get; set; }
public Vector Origin { get; } = projectile.AbsOrigin.Clone()!;
public CSmokeGrenadeProjectile Projectile { get; } = projectile;
public IPlayer Attacker { get; } = attacker;
}
[UsedImplicitly]
[EventHandler]
public void OnGameEnd(GameStateUpdateEvent ev) {
@@ -153,4 +143,13 @@ public class PoisonSmokeListener(IServiceProvider provider)
if (ev.Body.Killer == null || ev.Body.Killer.Id == ev.Body.OfPlayer.Id)
ev.IsCanceled = true;
}
private class PoisonEffect(CSmokeGrenadeProjectile projectile,
IOnlinePlayer attacker) {
public int Ticks { get; set; }
public int DamageGiven { get; set; }
public Vector Origin { get; } = projectile.AbsOrigin.Clone()!;
public CSmokeGrenadeProjectile Projectile { get; } = projectile;
public IPlayer Attacker { get; } = attacker;
}
}

View File

@@ -24,18 +24,18 @@ public static class SilentAWPServiceCollection {
public class SilentAWPItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider), IPluginModule {
private readonly SilentAWPConfig config =
provider.GetService<IStorage<SilentAWPConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new SilentAWPConfig();
private readonly IPlayerConverter<CCSPlayerController> playerConverter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IDictionary<string, int> silentShots =
new Dictionary<string, int>();
private SilentAWPConfig config
=> Provider.GetService<IStorage<SilentAWPConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new SilentAWPConfig();
public override string Name => Locale[SilentAWPMsgs.SHOP_ITEM_SILENT_AWP];
public override string Description

View File

@@ -1,5 +1,6 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs.Traitor;
@@ -7,7 +8,6 @@ using TTT.API.Events;
using TTT.API.Extensions;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
using TTT.CS2.Extensions;
using TTT.Game.Events.Body;
@@ -37,19 +37,23 @@ public class DamageStation(IServiceProvider provider)
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
private readonly Dictionary<string, StationInfo> killedWithStation = new();
public override string Name => Locale[StationMsgs.SHOP_ITEM_STATION_HURT];
public override string Description
=> Locale[StationMsgs.SHOP_ITEM_STATION_HURT_DESC];
private readonly ISet<string> killedWithStation = new HashSet<string>();
override protected void onInterval() {
var players = finder.GetOnline();
var toRemove = new List<CPhysicsPropMultiplayer>();
var playerMapping = players
.Select(p => (ApiPlayer: p, GamePlayer: converter.GetPlayer(p)))
.Where(m
=> m.GamePlayer != null
&& !Roles.GetRoles(m.ApiPlayer).Any(r => r is TraitorRole))
.ToList();
foreach (var (prop, info) in props) {
if (_Config.TotalHealthGiven != 0 && Math.Abs(info.HealthGiven)
> Math.Abs(_Config.TotalHealthGiven)) {
@@ -57,15 +61,14 @@ public class DamageStation(IServiceProvider provider)
continue;
}
var propPos = prop.AbsOrigin;
if (propPos == null) continue;
if (!prop.IsValid || prop.AbsOrigin == null) {
toRemove.Add(prop);
continue;
}
var playerMapping = players.Select(p
=> (ApiPlayer: p, GamePlayer: converter.GetPlayer(p)))
.Where(m => m.GamePlayer != null);
var propPos = prop.AbsOrigin;
var playerDists = playerMapping
.Where(t => !roles.GetRoles(t.ApiPlayer).OfType<TraitorRole>().Any())
.Select(t => (t.ApiPlayer, Origin: t.GamePlayer!.Pawn.Value?.AbsOrigin,
t.GamePlayer))
.Where(t => t is { Origin: not null, ApiPlayer.IsAlive: true })
@@ -87,23 +90,26 @@ public class DamageStation(IServiceProvider provider)
damageAmount = -dmgEvent.DmgDealt;
if (player.Health + damageAmount <= 0) {
killedWithStation.Add(player.Id);
killedWithStation[player.Id] = info;
var playerDeath = new PlayerDeathEvent(player)
.WithKiller(info.Owner as IOnlinePlayer)
.WithWeapon($"[{Name}]");
bus.Dispatch(playerDeath);
}
gamePlayer.EmitSound("Player.DamageFall", SELF(gamePlayer.Slot), 0.2f);
player.Health += damageAmount;
info.HealthGiven += damageAmount;
gamePlayer.EmitSound("Player.DamageFall", null, 0.2f);
}
}
foreach (var prop in toRemove) props.Remove(prop);
}
private static RecipientFilter SELF(int slot) {
return new RecipientFilter(slot);
}
[UsedImplicitly]
[EventHandler]
public void OnGameEnd(GameStateUpdateEvent ev) {
@@ -115,8 +121,11 @@ public class DamageStation(IServiceProvider provider)
[UsedImplicitly]
[EventHandler]
public void OnRagdollSpawn(BodyCreateEvent ev) {
if (!killedWithStation.Contains(ev.Body.OfPlayer.Id)) return;
if (ev.Body.Killer == null || ev.Body.Killer.Id == ev.Body.OfPlayer.Id)
ev.IsCanceled = true;
if (!killedWithStation.TryGetValue(ev.Body.OfPlayer.Id,
out var stationInfo))
return;
if (ev.Body.Killer != null && ev.Body.Killer.Id != ev.Body.OfPlayer.Id)
return;
ev.Body.Killer = stationInfo.Owner as IOnlinePlayer;
}
}

View File

@@ -10,7 +10,8 @@ using TTT.Game.Roles;
namespace TTT.CS2.Items.Station;
public static class HealthStationCollection {
public static void AddHealthStation(this IServiceCollection collection) {
public static void
AddHealthStationServices(this IServiceCollection collection) {
collection.AddModBehavior<HealthStation>();
}
}
@@ -36,8 +37,12 @@ public class HealthStation(IServiceProvider provider)
continue;
}
if (!prop.IsValid || prop.AbsOrigin == null) {
toRemove.Add(prop);
continue;
}
var propPos = prop.AbsOrigin;
if (propPos == null) continue;
var playerDists = players
.Select(p => (Player: p, Pos: p.Pawn.Value?.AbsOrigin))

View File

@@ -20,10 +20,10 @@ public abstract class StationItem<T>(IServiceProvider provider,
: RoleRestrictedItem<T>(provider), IPluginModule where T : IRole {
protected readonly StationConfig _Config = config;
protected readonly IPlayerConverter<CCSPlayerController> Converter =
protected readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly long PROP_SIZE_SQUARED = 500;
private readonly long PROP_SIZE_SQUARED = 700;
protected readonly Dictionary<CPhysicsPropMultiplayer, StationInfo> props =
new();
@@ -105,8 +105,8 @@ public abstract class StationItem<T>(IServiceProvider provider,
"weapon_deagle" => 40,
_ when Tag.PISTOLS.Contains(designerWeapon) => 10,
_ when Tag.SMGS.Contains(designerWeapon) => 15,
_ when Tag.SHOTGUNS.Contains(designerWeapon) => 25,
_ when Tag.RIFLES.Contains(designerWeapon) => 45,
_ when Tag.SHOTGUNS.Contains(designerWeapon) => 15,
_ when Tag.RIFLES.Contains(designerWeapon) => 35,
_ => 5
};
}
@@ -124,7 +124,7 @@ public abstract class StationItem<T>(IServiceProvider provider,
prop.SetModel("models/props/cs_office/microwave.vmdl");
prop.DispatchSpawn();
var gamePlayer = Converter.GetPlayer(player);
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null || !gamePlayer.Pawn.IsValid
|| gamePlayer.Pawn.Value == null)
return;

View File

@@ -0,0 +1,39 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game.Roles;
namespace TTT.CS2.Items.TeleportDecoy;
public static class TeleportDecoyServiceCollection {
public static void AddTeleportDecoyServices(
this IServiceCollection collection) {
collection.AddModBehavior<TeleportDecoyItem>();
collection.AddModBehavior<TeleportDecoyListener>();
}
}
public class TeleportDecoyItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider) {
public override string Name
=> Locale[TeleportDecoyMsgs.SHOP_ITEM_TELEPORT_DECOY];
public override string Description
=> Locale[TeleportDecoyMsgs.SHOP_ITEM_TELEPORT_DECOY_DESC];
private TeleportDecoyConfig config
=> Provider.GetService<IStorage<TeleportDecoyConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new TeleportDecoyConfig();
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) {
Inventory.GiveWeapon(player, new BaseWeapon("weapon_decoy"));
}
}

View File

@@ -0,0 +1,36 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API;
using TTT.API.Player;
namespace TTT.CS2.Items.TeleportDecoy;
public class TeleportDecoyListener(IServiceProvider provider) : IPluginModule {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IShop shop = provider.GetRequiredService<IShop>();
public void Dispose() { }
public void Start() { }
[UsedImplicitly]
[GameEventHandler]
public HookResult OnDecoyDetonate(EventDecoyDetonate ev, GameEventInfo _) {
if (ev.Userid == null) return HookResult.Continue;
var player = converter.GetPlayer(ev.Userid) as IOnlinePlayer;
if (player == null) return HookResult.Continue;
if (!shop.HasItem<TeleportDecoyItem>(player)) return HookResult.Continue;
shop.RemoveItem<TeleportDecoyItem>(player);
var vec = new Vector(ev.X, ev.Y, ev.Z + 16);
ev.Userid.Pawn.Value?.Teleport(vec);
return HookResult.Continue;
}
}

View File

@@ -0,0 +1,11 @@
using TTT.Locale;
namespace TTT.CS2.Items.TeleportDecoy;
public class TeleportDecoyMsgs {
public static IMsg SHOP_ITEM_TELEPORT_DECOY
=> MsgFactory.Create(nameof(SHOP_ITEM_TELEPORT_DECOY));
public static IMsg SHOP_ITEM_TELEPORT_DECOY_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_TELEPORT_DECOY_DESC));
}

View File

@@ -0,0 +1,106 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
using TTT.API;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.CS2.Extensions;
using TTT.CS2.RayTrace.Class;
using TTT.CS2.RayTrace.Enum;
using TTT.Game.Roles;
namespace TTT.CS2.Items.Tripwire;
public static class TripwireServiceCollection {
public static void AddTripwireServices(this IServiceCollection services) {
services.AddModBehavior<TripwireItem>();
}
}
public class TripwireItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider), IPluginModule {
public override string Name => Locale[TripwireMsgs.SHOP_ITEM_TRIPWIRE];
protected readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private TripwireConfig config = provider
.GetService<IStorage<TripwireConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new TripwireConfig();
public override string Description
=> Locale[TripwireMsgs.SHOP_ITEM_TRIPWIRE_DESC];
public override ShopItemConfig Config => config;
public void Start(BasePlugin? plugin) {
Start();
plugin
?.RegisterListener<
CounterStrikeSharp.API.Core.Listeners.OnServerPrecacheResources>(
onPrecache);
}
private void onPrecache(ResourceManifest manifest) {
manifest.AddResource(
"models/generic/conveyor_control_panel_01/conveyor_button_02.vmdl");
}
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
PurchaseResult result = base.CanPurchase(player);
if (result != PurchaseResult.SUCCESS) return result;
Server.NextWorldUpdateAsync(() => {
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) {
result = PurchaseResult.UNKNOWN_ERROR;
return;
}
var trace = gamePlayer.GetGameTraceByEyePosition(TraceMask.MaskSolid,
Contents.NoDraw, gamePlayer);
if (trace == null) {
result = PurchaseResult.TRIPWIRE_TOO_FAR;
return;
}
var endPos = trace.Value.EndPos;
var distance = gamePlayer.GetEyePosition()
.DistanceSquared(endPos.toVector());
if (distance > 2500) {
result = PurchaseResult.TRIPWIRE_TOO_FAR;
return;
}
})
.GetAwaiter()
.GetResult();
return result;
}
public override void OnPurchase(IOnlinePlayer player) {
Server.NextWorldUpdate(() => {
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
var trace = gamePlayer.GetGameTraceByEyePosition(TraceMask.MaskSolid,
Contents.NoDraw, gamePlayer);
if (trace == null) return;
var tripwire = Utilities.CreateEntityByName<CPhysicsProp>("prop_physics");
if (tripwire == null) return;
tripwire.SetModel(
"models/generic/conveyor_control_panel_01/conveyor_button_02.vmdl");
tripwire.DispatchSpawn();
tripwire.Teleport(trace.Value.EndPos.toVector());
});
}
}

View File

@@ -0,0 +1,11 @@
using TTT.Locale;
namespace TTT.CS2.Items.Tripwire;
public class TripwireMsgs {
public static IMsg SHOP_ITEM_TRIPWIRE
=> MsgFactory.Create(nameof(SHOP_ITEM_TRIPWIRE));
public static IMsg SHOP_ITEM_TRIPWIRE_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_TRIPWIRE_DESC));
}

View File

@@ -0,0 +1,81 @@
using System.Reactive.Concurrency;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.CS2.Extensions;
using TTT.CS2.lang;
using TTT.Game;
using TTT.Game.Events.Game;
using TTT.Game.Listeners;
namespace TTT.CS2.Listeners;
public class AfkTimerListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private IDisposable? specTimer, specWarnTimer;
private TTTConfig config
=> Provider.GetRequiredService<IStorage<TTTConfig>>()
.Load()
.GetAwaiter()
.GetResult() ?? new TTTConfig();
public override void Dispose() {
base.Dispose();
specTimer?.Dispose();
specWarnTimer?.Dispose();
}
[UsedImplicitly]
[EventHandler(IgnoreCanceled = true)]
public void OnRoundStart(GameStateUpdateEvent ev) {
if (ev.NewState != State.IN_PROGRESS) {
specTimer?.Dispose();
specWarnTimer?.Dispose();
return;
}
specWarnTimer?.Dispose();
specWarnTimer = Scheduler.Schedule(config.RoundCfg.CheckAFKTimespan / 2, ()
=> {
Server.NextWorldUpdate(() => {
foreach (var player in getAfkPlayers()) {
var apiPlayer = converter.GetPlayer(player);
var timetill = config.RoundCfg.CheckAFKTimespan / 2;
Messenger.Message(apiPlayer, Locale[CS2Msgs.AFK_WARNING(timetill)]);
}
});
});
specTimer?.Dispose();
specTimer = Scheduler.Schedule(config.RoundCfg.CheckAFKTimespan, () => {
Server.NextWorldUpdate(() => {
foreach (var player in getAfkPlayers()) {
var apiPlayer = converter.GetPlayer(player);
#if !DEBUG
player.ChangeTeam(CsTeam.Spectator);
#endif
Messenger.Message(apiPlayer, Locale[CS2Msgs.AFK_MOVED]);
}
});
});
}
private List<CCSPlayerController> getAfkPlayers() {
return Utilities.GetPlayers()
.Where(p => p.PlayerPawn.Value != null
&& p is { Team: CsTeam.CounterTerrorist or CsTeam.Terrorist }
&& p.GetHealth() >= 0 && !p.PlayerPawn.Value.HasMovedSinceSpawn)
.ToList();
}
}

View File

@@ -8,8 +8,8 @@ using TTT.API.Player;
using TTT.CS2.API;
using TTT.CS2.Events;
using TTT.CS2.Extensions;
using TTT.Game;
using TTT.Game.Events.Body;
using TTT.Game.lang;
using TTT.Game.Listeners;
using TTT.Game.Roles;
@@ -34,6 +34,8 @@ public class BodyPickupListener(IServiceProvider provider)
if (ev.Player is not IOnlinePlayer online)
throw new InvalidOperationException("Player is not an online player.");
if (ev.Player.Id == body.OfPlayer.Id) return;
var identifyEvent = new BodyIdentifyEvent(body, online);
Bus.Dispatch(identifyEvent);
@@ -57,7 +59,8 @@ public class BodyPickupListener(IServiceProvider provider)
if (ragdoll.IsValid) ragdoll.SetColor(primary.Color);
var online = converter.GetPlayer(ev.Body.OfPlayer);
if (online is not { IsValid: true }) return;
if (online is not { IsValid: true } || online.Team == CsTeam.Spectator)
return;
if (primary is InnocentRole) online.SwitchTeam(CsTeam.CounterTerrorist);

View File

@@ -15,12 +15,6 @@ using TTT.Karma.lang;
namespace TTT.CS2.Listeners;
public class KarmaBanner(IServiceProvider provider) : BaseListener(provider) {
private readonly KarmaConfig config =
provider.GetService<IStorage<KarmaConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new KarmaConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
@@ -31,6 +25,12 @@ public class KarmaBanner(IServiceProvider provider) : BaseListener(provider) {
private readonly Dictionary<IPlayer, DateTime> lastWarned = new();
private KarmaConfig config
=> Provider.GetService<IStorage<KarmaConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new KarmaConfig();
[UsedImplicitly]
[EventHandler(Priority = Priority.MONITOR, IgnoreCanceled = true)]
public void OnKarmaUpdate(KarmaUpdateEvent ev) {

View File

@@ -0,0 +1,48 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Player;
using TTT.Game.Events.Game;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
using TTT.Game.Roles;
namespace TTT.CS2.Listeners;
public class MapHookListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
[UsedImplicitly]
[EventHandler(Priority = Priority.MONITOR, IgnoreCanceled = true)]
public void OnRoleAssign(PlayerRoleAssignEvent ev) {
var player = converter.GetPlayer(ev.Player);
if (player == null) return;
switch (ev.Role) {
case TraitorRole:
player.Pawn.Value?.AcceptInput("AddContext", null, null, "TRAITOR:1");
break;
case DetectiveRole:
player.Pawn.Value?.AcceptInput("AddContext", null, null, "DETECTIVE:1");
break;
case InnocentRole:
player.Pawn.Value?.AcceptInput("AddContext", null, null, "INNOCENT:1");
break;
}
}
[UsedImplicitly]
[EventHandler]
public void OnRoundEnd(GameInitEvent ev) {
foreach (var player in Utilities.GetPlayers()) {
if (player.Pawn.Value == null) continue;
player.Pawn.Value.AcceptInput("RemoveContext", null, null, "TRAITOR");
player.Pawn.Value.AcceptInput("RemoveContext", null, null, "DETECTIVE");
player.Pawn.Value.AcceptInput("RemoveContext", null, null, "INNOCENT");
}
}
}

View File

@@ -20,8 +20,8 @@ public class PlayerStatsTracker(IServiceProvider provider) : IListener {
private readonly ISet<int> revealedDeaths = new HashSet<int>();
private readonly IDictionary<int, (int, int)> roundKillsAndAssists =
new Dictionary<int, (int, int)>();
private readonly IDictionary<int, RoundData> roundStats =
new Dictionary<int, RoundData>();
public void Dispose() { }
@@ -50,24 +50,39 @@ public class PlayerStatsTracker(IServiceProvider provider) : IListener {
ev.Assister == null ? null : converter.GetPlayer(ev.Assister);
if (killer != null) {
roundKillsAndAssists.TryGetValue(killer.Slot, out var def);
def.Item1++;
roundKillsAndAssists[killer.Slot] = def;
roundStats.TryGetValue(killer.Slot, out var def);
def ??= new RoundData();
def.Kills++;
roundStats[killer.Slot] = def;
}
if (assister != null && assister != killer) {
roundKillsAndAssists.TryGetValue(assister.Slot, out var def);
def.Item2++;
roundKillsAndAssists[assister.Slot] = def;
roundStats.TryGetValue(assister.Slot, out var def);
def ??= new RoundData();
def.Assists++;
roundStats[assister.Slot] = def;
}
}
[UsedImplicitly]
[EventHandler(Priority = Priority.HIGH)]
public void OnDamage(PlayerDamagedEvent ev) {
var attacker =
ev.Attacker == null ? null : converter.GetPlayer(ev.Attacker);
if (attacker == null) return;
roundStats.TryGetValue(attacker.Slot, out var def);
def ??= new RoundData();
def.Damage += ev.DmgDealt;
roundStats[attacker.Slot] = def;
}
[UsedImplicitly]
[EventHandler]
public void OnRoundEnd(GameStateUpdateEvent ev) {
if (ev.NewState == State.IN_PROGRESS) {
revealedDeaths.Clear();
roundKillsAndAssists.Clear();
roundStats.Clear();
return;
}
@@ -100,17 +115,24 @@ public class PlayerStatsTracker(IServiceProvider provider) : IListener {
var online = finder.GetOnline()
.Select(p => converter.GetPlayer(p))
.OfType<CCSPlayerController>()
.Where(p => p.IsValid && roundKillsAndAssists.ContainsKey(p.Slot));
.Where(p => p.IsValid && roundStats.ContainsKey(p.Slot));
foreach (var player in online) {
var stats = player.ActionTrackingServices?.MatchStats;
if (stats == null) continue;
var (kills, assists) = roundKillsAndAssists[player.Slot];
stats.Kills += kills;
stats.Assists += assists;
if (!roundStats.TryGetValue(player.Slot, out var data)) continue;
stats.Kills += data.Kills;
stats.Assists += data.Assists;
Utilities.SetStateChanged(player, "CCSPlayerController",
"m_pActionTrackingServices");
}
}
private record RoundData {
public int Assists;
public int Damage;
public int Kills;
}
}

View File

@@ -22,19 +22,16 @@ namespace TTT.CS2.Listeners;
public class RoundTimerListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly TTTConfig config = provider
.GetRequiredService<IStorage<TTTConfig>>()
.Load()
.GetAwaiter()
.GetResult() ?? new TTTConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IScheduler scheduler = provider
.GetRequiredService<IScheduler>();
public IDisposable? EndTimer;
private IDisposable? endTimer;
private TTTConfig config
=> Provider.GetRequiredService<IStorage<TTTConfig>>()
.Load()
.GetAwaiter()
.GetResult() ?? new TTTConfig();
[UsedImplicitly]
[EventHandler(IgnoreCanceled = true)]
@@ -45,7 +42,7 @@ public class RoundTimerListener(IServiceProvider provider)
.TotalSeconds);
Server.ExecuteCommand("mp_ignore_round_win_conditions 1");
foreach (var player in Utilities.GetPlayers()
.Where(p => p.LifeState != (int)LifeState_t.LIFE_ALIVE && p is {
.Where(p => p.GetHealth() <= 0 && p is {
Team: CsTeam.CounterTerrorist or CsTeam.Terrorist
}))
player.Respawn();
@@ -60,20 +57,20 @@ public class RoundTimerListener(IServiceProvider provider)
if (ev.NewState == State.IN_PROGRESS)
Server.NextWorldUpdate(() => {
foreach (var player in Utilities.GetPlayers()
.Where(p => p.LifeState != (int)LifeState_t.LIFE_ALIVE && p is {
.Where(p => p.GetHealth() <= 0 && p is {
Team: CsTeam.CounterTerrorist or CsTeam.Terrorist
}))
player.Respawn();
});
if (ev.NewState == State.FINISHED) endTimer?.Dispose();
if (ev.NewState == State.FINISHED) EndTimer?.Dispose();
if (ev.NewState != State.IN_PROGRESS) return;
var duration = config.RoundCfg.RoundDuration(ev.Game.Players.Count);
Server.NextWorldUpdate(()
=> RoundUtil.SetTimeRemaining((int)duration.TotalSeconds));
endTimer?.Dispose();
endTimer = scheduler.Schedule(duration,
EndTimer?.Dispose();
EndTimer = Scheduler.Schedule(duration,
() => {
Server.NextWorldUpdate(()
=> ev.Game.EndGame(EndReason.TIMEOUT(new InnocentRole(Provider))));
@@ -130,6 +127,7 @@ public class RoundTimerListener(IServiceProvider provider)
var role = Roles.GetRoles(player).FirstOrDefault();
if (role == null) continue;
csPlayer.SetClan(role.Name, false);
if (csPlayer.Team == CsTeam.Spectator) continue;
if (role is InnocentRole) csPlayer.SwitchTeam(CsTeam.CounterTerrorist);
}
@@ -139,6 +137,6 @@ public class RoundTimerListener(IServiceProvider provider)
public override void Dispose() {
base.Dispose();
endTimer?.Dispose();
EndTimer?.Dispose();
}
}

View File

@@ -0,0 +1,55 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Player;
using TTT.CS2.ThirdParties.eGO;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
using TTT.Game.Roles;
namespace TTT.CS2.Listeners;
public class WardenTagAssigner(IServiceProvider provider)
: BaseListener(provider) {
private Dictionary<string, (string, char)> oldTags =
new Dictionary<string, (string, char)>();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
[UsedImplicitly]
[EventHandler]
public void OnRoleAssign(PlayerRoleAssignEvent ev) {
var maul = EgoApi.MAUL.Get();
if (maul == null) return;
Server.NextWorldUpdate(() => {
var gamePlayer = converter.GetPlayer(ev.Player);
if (gamePlayer == null) return;
Task.Run(async () => {
if (ev.Role is DetectiveRole) {
var oldTag = await maul.getTagService().GetTag(gamePlayer.SteamID);
var oldTagColor =
await maul.getTagService().GetTagColor(gamePlayer.SteamID);
oldTags[ev.Player.Id] = (oldTag, oldTagColor);
}
await Server.NextWorldUpdateAsync(() => {
if (ev.Role is DetectiveRole) {
maul.getTagService().SetTag(gamePlayer, "[DETECTIVE]", false);
maul.getTagService()
.SetTagColor(gamePlayer, ChatColors.LightBlue, false);
} else if (oldTags.TryGetValue(ev.Player.Id, out var oldTag)) {
maul.getTagService().SetTag(gamePlayer, oldTag.Item1, false);
maul.getTagService().SetTagColor(gamePlayer, oldTag.Item2, false);
oldTags.Remove(ev.Player.Id);
}
});
});
});
}
}

View File

@@ -1,5 +1,7 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using JetBrains.Annotations;
using TTT.API;
using TTT.CS2.API;
@@ -7,6 +9,7 @@ namespace TTT.CS2.Player;
public class CS2AliveSpoofer : IAliveSpoofer, IPluginModule {
private readonly HashSet<CCSPlayerController> _fakeAlivePlayers = new();
private BasePlugin? plugin;
public ISet<CCSPlayerController> FakeAlivePlayers => _fakeAlivePlayers;
public void SpoofAlive(CCSPlayerController player) {
@@ -44,12 +47,31 @@ public class CS2AliveSpoofer : IAliveSpoofer, IPluginModule {
FakeAlivePlayers.Remove(player);
}
public void Dispose() { }
public void Dispose() {
_fakeAlivePlayers.Clear();
plugin?.RemoveListener<CounterStrikeSharp.API.Core.Listeners.OnTick>(
onTick);
}
public void Start() { }
public void Start(BasePlugin? plugin) {
if (plugin == null) return;
this.plugin = plugin;
plugin?.RegisterListener<CounterStrikeSharp.API.Core.Listeners.OnTick>(
onTick);
plugin?.RegisterListener<CounterStrikeSharp.API.Core.Listeners.OnMapStart>(
onMapStart);
}
private void onMapStart(string mapName) { _fakeAlivePlayers.Clear(); }
[UsedImplicitly]
[GameEventHandler]
public HookResult OnDisconnect(EventPlayerDisconnect ev, GameEventInfo _) {
if (ev.Userid == null) return HookResult.Continue;
_fakeAlivePlayers.Remove(ev.Userid);
return HookResult.Continue;
}
private void onTick() {

View File

@@ -1,5 +1,6 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using TTT.API.Player;
namespace TTT.CS2.Player;
@@ -47,17 +48,13 @@ public class CS2Player : IOnlinePlayer, IEquatable<CS2Player> {
}
}
private int namePadding
=> Math.Min(Utilities.GetPlayers().Select(p => p.PlayerName.Length).Max(),
24);
public bool Equals(CS2Player? other) {
if (other is null) return false;
return Id == other.Id;
}
public string Id { get; }
public string Name { get; }
public string Name { get; set; }
public int Health {
get => Player?.Pawn.Value != null ? Player.Pawn.Value.Health : 0;
@@ -100,7 +97,11 @@ public class CS2Player : IOnlinePlayer, IEquatable<CS2Player> {
}
public bool IsAlive {
get => Player != null && Player.Pawn.Value is { Health: > 0 };
get
=> Player != null && Player is {
Team : CsTeam.CounterTerrorist or CsTeam.Terrorist,
Pawn.Value.Health: > 0
};
set
=> throw new NotSupportedException(
@@ -118,9 +119,7 @@ public class CS2Player : IOnlinePlayer, IEquatable<CS2Player> {
// Goal: Pad the name to a fixed width for better alignment in logs
// Left-align ID, right-align name
private string createPaddedName() {
return CreatePaddedName(Id, Name, namePadding + 8);
}
private string createPaddedName() { return CreatePaddedName(Id, Name, 24); }
public static string CreatePaddedName(string id, string name, int len) {
var suffix = id.Length > 5 ? id[^5..] : id.PadLeft(5, '0');

View File

@@ -0,0 +1,21 @@
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Player;
using TTT.Game.Roles;
namespace TTT.CS2.Roles;
public class CS2TraitorRole(IServiceProvider provider) : TraitorRole(provider) {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public override void OnAssign(IOnlinePlayer player) {
base.OnAssign(player);
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
gamePlayer.AcceptInput("SetTargetName", null, null, "traitor");
if (gamePlayer.Pawn.Value != null) gamePlayer.Pawn.Value.Target = "traitor";
}
}

View File

@@ -0,0 +1,84 @@
using System.Runtime.InteropServices;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory;
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
namespace TTT.CS2.Utils;
public class DamageDealingHelper {
public static void DealDamage(CCSPlayerController target,
CCSPlayerController? attacker, int damage, string source,
DamageTypes_t type = DamageTypes_t.DMG_BLAST_SURFACE) {
if (target.Pawn.Value == null) return;
var infoSize = Schema.GetClassSize("CTakeDamageInfo");
var infoPtr = Marshal.AllocHGlobal(infoSize);
for (var i = 0; i < infoSize; i++) Marshal.WriteByte(infoPtr, i, 0);
var damageInfo = new CTakeDamageInfo(infoPtr);
Schema.SetSchemaValue(damageInfo.Handle, "CTakeDamageInfo", "m_hInflictor",
attacker != null ? attacker.Pawn.Raw : 0);
Schema.SetSchemaValue(damageInfo.Handle, "CTakeDamageInfo", "m_hAttacker",
attacker != null ? attacker.EntityHandle.Raw : 0);
damageInfo.Damage = damage;
damageInfo.BitsDamageType = type;
if (target.Pawn.Value?.AbsOrigin != null)
Schema.SetSchemaValue(damageInfo.Handle, "CTakeDamageInfo",
"m_vecDamagePosition",
target.Pawn.Value != null ?
target.Pawn.Value.AbsOrigin.Handle :
Vector.Zero.Handle);
Schema.SetSchemaValue(damageInfo.Handle, "CTakeDamageInfo",
"m_vecDamageForce", Vector.Zero.Handle);
var damageResultSize = Schema.GetClassSize("CTakeDamageResult");
var damageResultPtr = Marshal.AllocHGlobal(damageResultSize);
for (var i = 0; i < damageResultSize; i++)
Marshal.WriteByte(damageResultPtr, i, 0);
var damageResult = new CTakeDamageResult(damageResultPtr);
Schema.SetSchemaValue(damageResult.Handle, "CTakeDamageResult",
"m_pOriginatingInfo", damageInfo.Handle);
damageResult.HealthLost = damage;
damageResult.DamageDealt = damage;
damageResult.TotalledHealthLost = damage;
damageResult.TotalledDamageDealt = damage;
damageResult.WasDamageSuppressed = false;
if (target.EntityHandle.Value != null)
VirtualFunctions.CBaseEntity_TakeDamageOldFunc.Invoke(
target.EntityHandle.Value, damageInfo, damageResult);
Marshal.FreeHGlobal(infoPtr);
Marshal.FreeHGlobal(damageResultPtr);
}
}
[StructLayout(LayoutKind.Explicit)]
public struct CAttackerInfo {
[FieldOffset(0x0)]
public bool NeedInit;
[FieldOffset(0x1)]
public bool IsPawn;
[FieldOffset(0x2)]
public bool IsWorld;
[FieldOffset(0x4)]
public UInt32 AttackerPawn;
[FieldOffset(0x8)]
public ushort AttackerUserId;
[FieldOffset(0x0C)]
public int TeamChecked;
[FieldOffset(0x10)]
public int TeamNum;
}

Some files were not shown because too many files have changed in this diff Show More