Compare commits

...

105 Commits

Author SHA1 Message Date
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
Isaac
05aeb53a3c Refresh AliveSpoofer per map (#127) 2025-10-18 01:18:30 -07:00
Isaac
c545a10d6f Bug and crash fixes (#126) 2025-10-17 22:05:21 -07:00
167 changed files with 2656 additions and 381 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

@@ -0,0 +1,29 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Events;
using TTT.Game.Events.Game;
using TTT.Locale;
namespace SpecialRoundAPI;
public abstract class AbstractSpecialRound(IServiceProvider provider)
: ITerrorModule, IListener {
protected readonly IServiceProvider Provider = provider;
protected readonly ISpecialRoundTracker Tracker =
provider.GetRequiredService<ISpecialRoundTracker>();
public void Dispose() { }
public void Start() { }
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;
public record BhopRoundConfig : 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>

View File

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

View File

@@ -0,0 +1,8 @@
namespace SpecialRoundAPI;
public record SpeedRoundConfig : SpecialRoundConfig {
public override float Weight { get; init; } = 0.4f;
public TimeSpan InitialSeconds { get; init; } = TimeSpan.FromSeconds(60);
public TimeSpan SecondsPerKill { get; init; } = TimeSpan.FromSeconds(10);
}

View File

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

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

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

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

@@ -49,6 +49,7 @@ public static class CS2ServiceCollection {
collection
.AddModBehavior<IStorage<PoisonSmokeConfig>, CS2PoisonSmokeConfig>();
collection.AddModBehavior<IStorage<KarmaConfig>, CS2KarmaConfig>();
collection.AddModBehavior<IStorage<CamoConfig>, CS2CamoConfig>();
// TTT - CS2 Specific optionals
collection.AddScoped<ITextSpawner, TextSpawner>();
@@ -66,12 +67,15 @@ public static class CS2ServiceCollection {
collection.AddModBehavior<TeamChangeHandler>();
collection.AddModBehavior<TraitorChatHandler>();
collection.AddModBehavior<PlayerMuter>();
collection.AddModBehavior<MapChangeCausesEndListener>();
collection.AddModBehavior<EntityTargetHandlers>();
// Damage Cancelers
collection.AddModBehavior<OutOfRoundCanceler>();
collection.AddModBehavior<TaserListenCanceler>();
// Listeners
collection.AddModBehavior<AfkTimerListener>();
collection.AddModBehavior<BodyPickupListener>();
collection.AddModBehavior<IBodyTracker, BodyTracker>();
collection.AddModBehavior<LateSpawnListener>();
@@ -82,9 +86,7 @@ public static class CS2ServiceCollection {
collection.AddModBehavior<KarmaSyncer>();
// Commands
#if DEBUG
collection.AddModBehavior<TestCommand>();
#endif
collection.AddScoped<IGameManager, CS2GameManager>();
collection.AddScoped<IInventoryManager, CS2InventoryManager>();

View File

@@ -8,6 +8,7 @@ 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

@@ -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 {
public void Dispose() { }
public void Start() { }
private BasePlugin? plugin;
public string Id => "reload";
public void Start(BasePlugin? plugin) {
if (plugin == null) return;
this.plugin = plugin;
}
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);
}
}

View File

@@ -0,0 +1,42 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory;
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.AcceptInput("AddContext", null, null, "TRAITOR:1");
info.ReplySync("Target: " + gamePlayer.Target);
gamePlayer.Target = "TRAITOR";
info.ReplySync("New Target: " + gamePlayer.Target);
if (gamePlayer.Pawn.Value != null)
gamePlayer.Pawn.Value.Globalname = "TRAITOR";
info.ReplySync("New Globalname: " + gamePlayer.Pawn.Value?.Globalname);
gamePlayer.AcceptInput("name", null, null, "TRAITOR");
gamePlayer.AcceptInput("targetname", null, null, "TRAITOR");
gamePlayer.AddEntityIOEvent("targetname", null, null, "TRAITOR");
gamePlayer.Globalname = "TRAITOR";
});
return Task.FromResult(CommandResult.SUCCESS);
}
}

View File

@@ -0,0 +1,41 @@
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.GetType().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);
}
tracker.TryStartSpecialRound(round);
info.ReplySync($"Started special round '{roundName}'.");
return Task.FromResult(CommandResult.SUCCESS);
}
}

View File

@@ -27,12 +27,18 @@ public class TestCommand(IServiceProvider provider) : ICommand, IPluginModule {
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(

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,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.4f, 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

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

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

@@ -45,6 +45,7 @@ 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);
info.DontBroadcast = true;
spoofer.SpoofAlive(player);
Server.NextWorldUpdateAsync(() => bus.Dispatch(deathEvent));
@@ -67,13 +68,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;

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

@@ -0,0 +1,64 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Messages;
namespace TTT.CS2.GameHandlers;
public class EntityTargetHandlers(IServiceProvider provider) : IPluginModule {
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
plugin?.HookEntityOutput("*", "*", handler);
}
private HookResult handler(CEntityIOOutput output, string name,
CEntityInstance activator, CEntityInstance caller, CVariant value,
float delay) {
if (caller.DesignerName == "prop_dynamic") return HookResult.Continue;
messenger.Debug("Entity Output Triggered: " + name);
messenger.Debug("Activator: " + activator.DesignerName);
messenger.Debug("Caller: " + caller.DesignerName);
messenger.Debug("Value: " + value + " " + value.GetType());
caller.AcceptInput("OnPass");
activator.AcceptInput("OnPass");
if (caller.DesignerName != "filter_activator_name")
return HookResult.Continue;
var csPlayer =
Utilities.GetPlayerFromIndex((int)activator.EntityHandle.Index);
if (csPlayer != null && csPlayer.IsValid) {
messenger.DebugAnnounce(
$"Filter Activator Name triggered by player: {csPlayer.PlayerName} {(int)csPlayer.Index}");
}
var ptrPlayer = new CCSPlayerController(activator.Handle);
if (ptrPlayer.IsValid) {
messenger.DebugAnnounce(
$"Filter Activator Name triggered by player controller: {ptrPlayer.PlayerName} {(int)ptrPlayer.Index}");
}
messenger.DebugAnnounce(output + " - " + output.Description);
var connections = output.Connections;
if (connections != null) debugConnection(connections);
caller.AcceptInput("OnPass");
return HookResult.Continue;
}
private void debugConnection(EntityIOConnection_t connection) {
messenger.DebugAnnounce("Connection:");
messenger.DebugAnnounce(" Target: " + connection.Target);
messenger.DebugAnnounce(" Input: " + connection.TargetInput);
messenger.DebugAnnounce(" Parameter: " + connection.ValueOverride);
messenger.DebugAnnounce(" Delay: " + connection.Delay);
messenger.DebugAnnounce(" Times to fire: " + connection.TimesToFire);
if (connection.Next != null) debugConnection(connection.Next);
}
}

View File

@@ -33,7 +33,7 @@ public class LateSpawnListener(IServiceProvider provider)
[UsedImplicitly]
[EventHandler]
public void GameState(GameStateUpdateEvent ev) {
if (ev.NewState == State.FINISHED) return;
if (ev.NewState is State.FINISHED or State.WAITING) return;
Server.NextWorldUpdate(() => {
foreach (var player in Utilities.GetPlayers()

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

@@ -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;
@@ -21,6 +24,9 @@ public class PlayerMuter(IServiceProvider provider) : IPluginModule {
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
private readonly IGameManager game =
provider.GetRequiredService<IGameManager>();
public void Dispose() { }
public void Start() { }
@@ -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,14 @@ 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

@@ -90,9 +90,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

@@ -75,6 +75,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 +104,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,9 +12,11 @@ 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 TTTConfig config
=> provider.GetService<IStorage<TTTConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new TTTConfig();
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();

View File

@@ -9,7 +9,9 @@ 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;
using TTT.Game.Events.Player;
namespace TTT.CS2.GameHandlers;
@@ -23,6 +25,9 @@ public class TeamChangeHandler(IServiceProvider provider) : IPluginModule {
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
private readonly IBodyTracker bodies =
provider.GetRequiredService<IBodyTracker>();
public void Dispose() { }
public void Start() { }
@@ -34,6 +39,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
@@ -45,15 +52,21 @@ public class TeamChangeHandler(IServiceProvider provider) : IPluginModule {
};
if (games.ActiveGame is not { State: State.IN_PROGRESS }) {
if (player != null && player.GetHealth() <= 0)
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;
@@ -30,43 +32,65 @@ public class TraitorChatHandler(IServiceProvider provider) : IPluginModule {
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
private readonly IMuted? mutedPlayers = provider.GetService<IMuted>();
private IActain? maulService;
public void Start(BasePlugin? plugin) {
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
&& 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,11 +16,11 @@ 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 ArmorConfig config
=> Provider.GetService<IStorage<ArmorConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new ArmorConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();

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,8 +17,8 @@ public class BodyPaintListener(IServiceProvider provider)
private readonly IBodyTracker bodies =
provider.GetRequiredService<IBodyTracker>();
private readonly BodyPaintConfig config =
provider.GetService<IStorage<BodyPaintConfig>>()
private BodyPaintConfig config
=> Provider.GetService<IStorage<BodyPaintConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new BodyPaintConfig();

View File

@@ -18,9 +18,11 @@ 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 CamoConfig config
=> Provider.GetService<IStorage<CamoConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new CamoConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();

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

@@ -13,12 +13,13 @@ 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>>()
private ClusterGrenadeConfig config
=> provider.GetService<IStorage<ClusterGrenadeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new ClusterGrenadeConfig();

View File

@@ -28,12 +28,12 @@ namespace TTT.CS2.Items.Compass;
/// </summary>
public abstract class AbstractCompassItem<TRole> : RoleRestrictedItem<TRole>,
IListener, IPluginModule where TRole : class, IRole {
protected readonly CompassConfig config;
protected CompassConfig _Config { get; }
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 +42,7 @@ public abstract class AbstractCompassItem<TRole> : RoleRestrictedItem<TRole>,
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
}
public override ShopItemConfig Config => config;
public override ShopItemConfig Config => _Config;
public void Start(BasePlugin? plugin) {
base.Start();
@@ -80,6 +80,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 +96,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 +121,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

@@ -28,11 +28,11 @@ 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 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>();

View File

@@ -1,6 +1,7 @@
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

@@ -13,8 +13,8 @@ namespace TTT.CS2.Items.OneHitKnife;
public class OneHitKnifeListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly OneHitKnifeConfig config =
provider.GetService<IStorage<OneHitKnifeConfig>>()
private OneHitKnifeConfig config
=> Provider.GetService<IStorage<OneHitKnifeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new OneHitKnifeConfig();

View File

@@ -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,8 +24,8 @@ public class PoisonShotsListener(IServiceProvider provider)
: BaseListener(provider), IPluginModule {
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly PoisonShotsConfig config =
provider.GetService<IStorage<PoisonShotsConfig>>()
private PoisonShotsConfig config
=> Provider.GetService<IStorage<PoisonShotsConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new PoisonShotsConfig();

View File

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

@@ -25,8 +25,8 @@ namespace TTT.CS2.Items.PoisonSmoke;
public class PoisonSmokeListener(IServiceProvider provider)
: BaseListener(provider), IPluginModule {
private readonly PoisonSmokeConfig config =
provider.GetService<IStorage<PoisonSmokeConfig>>()
private PoisonSmokeConfig config
=> Provider.GetService<IStorage<PoisonSmokeConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new PoisonSmokeConfig();

View File

@@ -24,8 +24,8 @@ public static class SilentAWPServiceCollection {
public class SilentAWPItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider), IPluginModule {
private readonly SilentAWPConfig config =
provider.GetService<IStorage<SilentAWPConfig>>()
private SilentAWPConfig config
=> Provider.GetService<IStorage<SilentAWPConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new SilentAWPConfig();

View File

@@ -59,8 +59,12 @@ public class DamageStation(IServiceProvider provider)
continue;
}
if (!prop.IsValid || prop.AbsOrigin == null) {
toRemove.Add(prop);
continue;
}
var propPos = prop.AbsOrigin;
if (propPos == null) continue;
var playerMapping = players.Select(p
=> (ApiPlayer: p, GamePlayer: converter.GetPlayer(p)))

View File

@@ -36,8 +36,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

@@ -0,0 +1,84 @@
using System.Drawing;
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.CS2.Utils;
using TTT.Game;
using TTT.Game.Events.Game;
using TTT.Game.Listeners;
using TTT.Game.Roles;
namespace TTT.CS2.Listeners;
public class AfkTimerListener(IServiceProvider provider)
: BaseListener(provider) {
private TTTConfig config
=> Provider.GetRequiredService<IStorage<TTTConfig>>()
.Load()
.GetAwaiter()
.GetResult() ?? new TTTConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private IDisposable? specTimer, specWarnTimer;
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

@@ -10,6 +10,7 @@ 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;
@@ -57,7 +58,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,8 +15,8 @@ using TTT.Karma.lang;
namespace TTT.CS2.Listeners;
public class KarmaBanner(IServiceProvider provider) : BaseListener(provider) {
private readonly KarmaConfig config =
provider.GetService<IStorage<KarmaConfig>>()
private KarmaConfig config
=> Provider.GetService<IStorage<KarmaConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new KarmaConfig();

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 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>();
private IDisposable? endTimer;
public IDisposable? EndTimer;
[UsedImplicitly]
[EventHandler(IgnoreCanceled = true)]
@@ -66,14 +63,14 @@ public class RoundTimerListener(IServiceProvider provider)
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

@@ -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;
@@ -8,6 +10,7 @@ namespace TTT.CS2.Player;
public class CS2AliveSpoofer : IAliveSpoofer, IPluginModule {
private readonly HashSet<CCSPlayerController> _fakeAlivePlayers = new();
public ISet<CCSPlayerController> FakeAlivePlayers => _fakeAlivePlayers;
private BasePlugin? plugin = null;
public void SpoofAlive(CCSPlayerController player) {
if (player.IsBot) {
@@ -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

@@ -47,10 +47,6 @@ 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;
@@ -119,6 +115,11 @@ 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() {
var onlineLengths = Utilities.GetPlayers()
.Select(p => p.PlayerName.Length)
.ToList();
if (onlineLengths.Count == 0) return CreatePaddedName(Id, Name, 13);
var namePadding = Math.Min(onlineLengths.Max(), 24);
return CreatePaddedName(Id, Name, namePadding + 8);
}

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

@@ -2,10 +2,8 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
using TTT.CS2.RayTrace.Class;
using Address = TTT.CS2.Utils.Address;
namespace TTT.CS2.Items.ClusterGrenade;
namespace TTT.CS2.Utils;
public class GrenadeDataHelper {
private static readonly CHEGrenadeProjectile_CreateDelegate

View File

@@ -50,7 +50,8 @@ public static class RoundUtil {
public static void EndRound(RoundEndReason reason) {
var gameRules = ServerUtil.GameRulesProxy;
if (gameRules == null || gameRules.GameRules == null) return;
if (gameRules == null || gameRules.GameRules == null || !gameRules.IsValid)
return;
// TODO: Figure out what these params do
// TerminateRoundFunc.Invoke(gameRules.GameRules.Handle, 5f, reason, 0, 0);
VirtualFunctions.TerminateRoundFunc.Invoke(gameRules.GameRules.Handle,

View File

@@ -11,7 +11,9 @@ public static class ServerUtil {
.FindAllEntitiesByDesignerName<CCSGameRulesProxy>("cs_gamerules")
.FirstOrDefault();
return GameRulesProxy?.GameRules;
if (GameRulesProxy == null || !GameRulesProxy.IsValid) return null;
return GameRulesProxy.GameRules;
}
}

View File

@@ -0,0 +1,127 @@
namespace TTT.CS2.Utils;
public static class WeaponTranslations {
public static string GetFriendlyWeaponName(string designerName) {
switch (designerName) {
case "weapon_ak47":
return "AK-47";
case "weapon_aug":
return "AUG";
case "weapon_awp":
return "AWP";
case "weapon_bizon":
return "Bizon";
case "weapon_cz75a":
return "CZ75";
case "weapon_deagle":
return "Desert Eagle";
case "weapon_elite":
return "Dualies";
case "weapon_famas":
return "Famas";
case "weapon_fiveseven":
return "Five Seven";
case "weapon_g3sg1":
return "G3SG1";
case "weapon_galilar":
return "Galil";
case "weapon_glock":
return "Glock 18";
case "weapon_hkp2000":
return "HPK2000";
case "weapon_m249":
return "M249";
case "weapon_m4a1":
return "M4A1";
case "weapon_m4a1_silencer":
return "M4A1-S";
case "weapon_m4a4":
return "M4A4";
case "weapon_mac10":
return "MAC10";
case "weapon_mag7":
return "MAG7";
case "weapon_mp5sd":
return "MP5SD";
case "weapon_mp7":
return "MP7";
case "weapon_mp9":
return "MP9";
case "weapon_negev":
return "Negev";
case "weapon_nova":
return "Nova";
case "weapon_p250":
return "P250";
case "weapon_p90":
return "P90";
case "weapon_revolver":
return "Revolver";
case "weapon_sawedoff":
return "Sawed Off";
case "weapon_scar20":
return "Scar20";
case "weapon_sg553":
return "SG553";
case "weapon_sg556":
return "SG556";
case "weapon_ssg08":
return "SSG08";
case "weapon_taser":
return "Zeus";
case "weapon_tec9":
return "Tec9";
case "weapon_ump45":
return "UMP45";
case "weapon_usp_silencer":
return "USPS";
case "weapon_xm1014":
return "XM1014";
case "item_kevlar":
return "Kevlar";
case "item_assaultsuit":
return "Kevlar Helmet";
case "weapon_snowball":
return "Snowball";
case "weapon_shield":
return "Shield";
case "weapon_c4":
return "Bomb";
case "weapon_healthshot":
return "Healthshot";
case "weapon_breachcharge":
return "Breach Charge";
case "weapon_tablet":
return "Tablet";
case "weapon_bumpmine":
return "Bumpmine";
case "weapon_smokegrenade":
return "Smoke Grenade";
case "weapon_flashbang":
return "Flashbang";
case "weapon_hegrenade":
return "HE Grenade";
case "weapon_molotov":
return "Molotov";
case "weapon_incgrenade":
return "Incendiary Grenade";
case "weapon_decoy":
return "Decoy Grenade";
case "weapon_tagrenade":
return "TAGrenade";
case "weapon_frag":
return "Frag Grenade";
case "weapon_firebomb":
return "Firebomb";
case "weapon_diversion":
return "Diversion";
case "weapon_knife_t":
case "weapon_knife":
return "Knife";
default: {
var name = designerName.Replace("weapon_", "");
return char.ToUpper(name[0]) + name[1..];
}
}
}
}

View File

@@ -1,6 +1,7 @@
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.CS2.lang;
@@ -18,6 +19,12 @@ public static class CS2Msgs {
rolePrefix + scannedPlayer.Name, role.Name);
}
public static IMsg AFK_WARNING(TimeSpan span) {
return MsgFactory.Create(nameof(AFK_WARNING), span.TotalSeconds);
}
public static IMsg AFK_MOVED => MsgFactory.Create(nameof(AFK_MOVED));
public static IMsg TRAITOR_CHAT_FORMAT(IOnlinePlayer player, string msg) {
return MsgFactory.Create(nameof(TRAITOR_CHAT_FORMAT), player.Name, msg);
}

View File

@@ -2,6 +2,8 @@ ROLE_SPECTATOR: "Spectator"
TRAITOR_CHAT_FORMAT: "{darkred}[TRAITORS] {red}{0}: {default}{1}"
TASER_SCANNED: "%PREFIX%You scanned {0}{grey}, they are %an% {1}{grey}!"
DNA_PREFIX: "{darkblue}D{blue}N{lightblue}A{grey} | {grey}"
AFK_WARNING: "%PREFIX%You will be moved to spectators in {yellow}{0} second%s%{grey} for being AFK."
AFK_MOVED: "%PREFIX%You were moved to spectators for being AFK."
DEAD_MUTE_REMINDER: "%PREFIX%You are dead and cannot be heard."

View File

@@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Command;
using TTT.API.Player;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.Game.Commands;

View File

@@ -3,6 +3,7 @@ using TTT.API.Command;
using TTT.API.Game;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.Game.Commands;

View File

@@ -4,6 +4,7 @@ using TTT.API;
using TTT.API.Command;
using TTT.API.Events;
using TTT.API.Player;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.Game.Commands;

View File

@@ -12,4 +12,8 @@
<ProjectReference Include="..\API\API.csproj"/>
</ItemGroup>
<ItemGroup>
<Folder Include="Listeners\Stats\" />
</ItemGroup>
</Project>

View File

@@ -11,11 +11,9 @@ namespace TTT.Game.Listeners;
public class GameRestartListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly TTTConfig config = provider
.GetRequiredService<IStorage<TTTConfig>>()
.Load()
.GetAwaiter()
.GetResult() ?? new TTTConfig();
private TTTConfig config =
provider.GetService<IStorage<TTTConfig>>()?.Load().GetAwaiter().GetResult()
?? new TTTConfig();
private readonly IScheduler scheduler =
provider.GetRequiredService<IScheduler>();

View File

@@ -1,6 +1,7 @@
using JetBrains.Annotations;
using TTT.API.Events;
using TTT.Game.Events.Player;
using TTT.Game.lang;
namespace TTT.Game.Listeners;

View File

@@ -9,9 +9,11 @@ namespace TTT.Game.Listeners;
public class PlayerJoinStarting(IServiceProvider provider)
: BaseListener(provider) {
private readonly TTTConfig config =
provider.GetService<IStorage<TTTConfig>>()?.Load().GetAwaiter().GetResult()
?? new TTTConfig();
private TTTConfig config
=> Provider.GetService<IStorage<TTTConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new TTTConfig();
[EventHandler]
[UsedImplicitly]

View File

@@ -3,6 +3,7 @@ using JetBrains.Annotations;
using TTT.API.Events;
using TTT.API.Game;
using TTT.Game.Events.Game;
using TTT.Game.lang;
using TTT.Game.Roles;
namespace TTT.Game.Listeners;

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection;
using TTT.API.Game;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.Game.Loggers;
@@ -60,6 +61,15 @@ public class SimpleLogger(IServiceProvider provider) : IActionLogger {
msg.Value.BackgroundMsg(player, locale[GameMsgs.GAME_LOGS_FOOTER]);
}
public string[] MakeLogs() {
List<string> logLines = [];
logLines.Add(locale[GameMsgs.GAME_LOGS_HEADER]);
foreach (var (time, action) in GetActions())
logLines.Add($"{formatTime(time)} {action.Format()}");
logLines.Add(locale[GameMsgs.GAME_LOGS_FOOTER]);
return logLines.ToArray();
}
private string formatTime(DateTime time) {
if (epoch == null) return time.ToString("o");
var span = time - epoch.Value;

View File

@@ -4,16 +4,17 @@ using TTT.API.Messages;
using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.Game.Roles;
public abstract class BaseRole(IServiceProvider provider) : IRole {
protected readonly TTTConfig Config = provider
.GetRequiredService<IStorage<TTTConfig>>()
.Load()
.GetAwaiter()
.GetResult() ?? new TTTConfig();
protected TTTConfig Config
=> Provider.GetRequiredService<IStorage<TTTConfig>>()
.Load()
.GetAwaiter()
.GetResult() ?? new TTTConfig();
protected readonly IInventoryManager Inventory =
provider.GetRequiredService<IInventoryManager>();

View File

@@ -1,5 +1,6 @@
using System.Drawing;
using TTT.API.Player;
using TTT.Game.lang;
namespace TTT.Game.Roles;

View File

@@ -1,6 +1,7 @@
using System.Drawing;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Player;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.Game.Roles;

View File

@@ -1,5 +1,6 @@
using System.Drawing;
using TTT.API.Player;
using TTT.Game.lang;
namespace TTT.Game.Roles;

View File

@@ -8,6 +8,7 @@ using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
using TTT.Game.Events.Game;
using TTT.Game.lang;
using TTT.Game.Loggers;
using TTT.Game.Roles;
using TTT.Locale;
@@ -15,11 +16,11 @@ using TTT.Locale;
namespace TTT.Game;
public class RoundBasedGame(IServiceProvider provider) : IGame {
private readonly TTTConfig config = provider
.GetRequiredService<IStorage<TTTConfig>>()
.Load()
.GetAwaiter()
.GetResult() ?? new TTTConfig();
private TTTConfig config
=> provider.GetRequiredService<IStorage<TTTConfig>>()
.Load()
.GetAwaiter()
.GetResult() ?? new TTTConfig();
protected readonly IMsgLocalizer Locale =
provider.GetRequiredService<IMsgLocalizer>();
@@ -147,10 +148,11 @@ public class RoundBasedGame(IServiceProvider provider) : IGame {
case > 0 when nonTraitorsAlive == 0:
winningTeam = traitorRole;
return true;
case > 0 when nonTraitorsAlive == detectivesAlive:
winningTeam = detectiveRole;
return true;
case 0 when nonTraitorsAlive > 0:
winningTeam = nonTraitorsAlive == detectivesAlive ?
detectiveRole :
innocentRole;
winningTeam = innocentRole;
return true;
default:
winningTeam = null;

View File

@@ -38,6 +38,7 @@ public record TTTConfig {
public record RoundConfig {
public TimeSpan CountDownDuration { get; init; } = TimeSpan.FromSeconds(10);
public TimeSpan TimeBetweenRounds { get; init; } = TimeSpan.FromSeconds(5);
public TimeSpan CheckAFKTimespan { get; init; } = TimeSpan.FromSeconds(60);
public int MinimumPlayers { get; init; } = 2;
public virtual TimeSpan RoundDuration(int players) {

View File

@@ -4,7 +4,7 @@ using TTT.API.Role;
using TTT.Game.Roles;
using TTT.Locale;
namespace TTT.Game;
namespace TTT.Game.lang;
public static class GameMsgs {
public static IMsg PREFIX => MsgFactory.Create(nameof(PREFIX));

View File

@@ -11,11 +11,4 @@
<ProjectReference Include="..\Game\Game.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66"/>
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.9"/>
<PackageReference Include="MySqlConnector" Version="2.4.0"/>
<PackageReference Include="SQLite" Version="3.13.0"/>
</ItemGroup>
</Project>

View File

@@ -48,9 +48,9 @@ public record KarmaConfig {
/// <summary>
/// Amount of karma a player will gain at the end of each round.
/// </summary>
public int KarmaPerRound { get; init; } = 3;
public int KarmaPerRound { get; init; } = 1;
public int KarmaPerRoundWin { get; init; } = 5;
public int KarmaPerRoundWin { get; init; } = 2;
public int INNO_ON_TRAITOR { get; init; } = 5;
public int TRAITOR_ON_DETECTIVE { get; init; } = 1;

View File

@@ -15,8 +15,8 @@ namespace TTT.Karma;
public class KarmaListener(IServiceProvider provider) : BaseListener(provider) {
private readonly Dictionary<string, int> badKills = new();
private readonly KarmaConfig config =
provider.GetService<IStorage<KarmaConfig>>()
private KarmaConfig config
=> Provider.GetService<IStorage<KarmaConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new KarmaConfig();

View File

@@ -4,8 +4,6 @@ using System.Diagnostics;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using Dapper;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Player;
@@ -15,151 +13,38 @@ using TTT.Karma.Events;
namespace TTT.Karma;
public sealed class KarmaStorage(IServiceProvider provider) : IKarmaService {
// Toggle immediate writes. If false, every Write triggers a flush
private const bool EnableCache = true;
private readonly IEventBus _bus = provider.GetRequiredService<IEventBus>();
private readonly HttpClient client =
provider.GetRequiredService<HttpClient>();
private readonly IStorage<KarmaConfig>? _configStorage =
provider.GetService<IStorage<KarmaConfig>>();
private KarmaConfig config
=> provider.GetService<IStorage<KarmaConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new KarmaConfig();
private readonly SemaphoreSlim _flushGate = new(1, 1);
public async Task<int> Load(IPlayer key) {
var result = await client.GetAsync("user/" + key.Id);
// Cache keyed by stable player id to avoid relying on IPlayer equality
private readonly ConcurrentDictionary<string, int> _karmaCache = new();
if (!result.IsSuccessStatusCode) return config.DefaultKarma;
private readonly IScheduler _scheduler =
provider.GetRequiredService<IScheduler>();
var content = await result.Content.ReadAsStringAsync();
var json = System.Text.Json.JsonDocument.Parse(content);
if (!json.RootElement.TryGetProperty("karma", out var karmaElement))
return config.DefaultKarma;
private KarmaConfig _config = new();
private IDbConnection? _connection;
private IDisposable? _flushSubscription;
public string Id => nameof(KarmaStorage);
public string Version => GitVersionInformation.FullSemVer;
public void Start() {
// Load configuration first
if (_configStorage is not null)
// Synchronously wait here since IKarmaService.Start is sync
_config = _configStorage.Load().GetAwaiter().GetResult()
?? new KarmaConfig();
// Open a dedicated connection used only by this service
_connection = new SqliteConnection(_config.DbString);
_connection.Open();
// Ensure schema before any reads or writes
_connection.Execute(@"CREATE TABLE IF NOT EXISTS PlayerKarma (
PlayerId TEXT PRIMARY KEY,
Karma INTEGER NOT NULL
)");
// Periodic flush with proper error handling and serialization
_flushSubscription = Observable
.Interval(TimeSpan.FromSeconds(30), _scheduler)
.SelectMany(_ => FlushAsync().ToObservable())
.Subscribe(_ => { }, // no-op on success
ex => {
// Replace with your logger if available
Trace.TraceError($"Karma flush failed: {ex}");
});
return karmaElement.GetInt32();
}
public async Task<int> Load(IPlayer player) {
if (player is null) throw new ArgumentNullException(nameof(player));
var key = player.Id;
public Task Write(IPlayer key, int newData) {
var data = new { steam_id = key.Id, karma = newData };
if (EnableCache && _karmaCache.TryGetValue(key, out var cached))
return cached;
var payload = new StringContent(
System.Text.Json.JsonSerializer.Serialize(data),
System.Text.Encoding.UTF8, "application/json");
var conn = EnsureConnection();
// Parameterize the default value to keep SQL static
var sql = @"
SELECT COALESCE(
(SELECT Karma FROM PlayerKarma WHERE PlayerId = @PlayerId),
@DefaultKarma
)";
var karma = await conn.QuerySingleAsync<int>(sql,
new { PlayerId = key, _config.DefaultKarma });
if (EnableCache) _karmaCache[key] = karma;
return karma;
return client.PatchAsync("user/" + key.Id, payload);
}
public async Task Write(IPlayer player, int newValue) {
if (player is null) throw new ArgumentNullException(nameof(player));
var key = player.Id;
var max = _config.MaxKarma(player);
if (newValue > max)
throw new ArgumentOutOfRangeException(nameof(newValue),
$"Karma must be less than {max} for player {key}.");
int oldValue;
if (!_karmaCache.TryGetValue(key, out oldValue))
oldValue = await Load(player);
if (oldValue == newValue) return;
var evt = new KarmaUpdateEvent(player, oldValue, newValue);
try { _bus.Dispatch(evt); } catch {
// Replace with your logger if available
Trace.TraceError("Exception during KarmaUpdateEvent dispatch.");
throw;
}
if (evt.IsCanceled) return;
_karmaCache[key] = newValue;
if (!EnableCache) await FlushAsync();
}
public void Dispose() {
try {
_flushSubscription?.Dispose();
// Best effort final flush
if (_connection is { State: ConnectionState.Open })
FlushAsync().GetAwaiter().GetResult();
} catch (Exception ex) {
Trace.TraceError($"Dispose flush failed: {ex}");
} finally {
_connection?.Dispose();
_flushGate.Dispose();
}
}
private async Task FlushAsync() {
var conn = EnsureConnection();
// Fast path if there is nothing to flush
if (_karmaCache.IsEmpty) return;
await _flushGate.WaitAsync().ConfigureAwait(false);
try {
// Snapshot to avoid long lock on dictionary and to provide a stable view
var snapshot = _karmaCache.ToArray();
if (snapshot.Length == 0) return;
using var tx = conn.BeginTransaction();
const string upsert = @"
INSERT INTO PlayerKarma (PlayerId, Karma)
VALUES (@PlayerId, @Karma)
ON CONFLICT(PlayerId) DO UPDATE SET Karma = excluded.Karma
";
foreach (var (playerId, karma) in snapshot)
await conn.ExecuteAsync(upsert,
new { PlayerId = playerId, Karma = karma }, tx);
tx.Commit();
} finally { _flushGate.Release(); }
}
private IDbConnection EnsureConnection() {
if (_connection is not { State: ConnectionState.Open })
throw new InvalidOperationException(
"Storage connection is not initialized.");
return _connection;
}
public void Dispose() { }
public void Start() { }
}

View File

@@ -12,6 +12,9 @@
<ProjectReference Include="..\CS2\CS2.csproj"/>
<ProjectReference Include="..\Karma\Karma.csproj"/>
<ProjectReference Include="..\Shop\Shop.csproj"/>
<ProjectReference Include="..\RTD\RTD.csproj"/>
<ProjectReference Include="..\Stats\Stats.csproj"/>
<ProjectReference Include="..\SpecialRound\SpecialRound.csproj"/>
</ItemGroup>
<ItemGroup>

View File

@@ -36,6 +36,8 @@ public class TTT(IServiceProvider provider) : BasePlugin {
$"Found {pluginModules.Count} plugin modules, loading...");
foreach (var module in pluginModules) {
Logger.LogInformation(
$"Registering {module.Version} {module.Id} {module.GetType().Namespace}");
module.Start(this, hotReload);
RegisterAllAttributes(module);
loadedModules.Add(module);
@@ -62,4 +64,5 @@ public class TTT(IServiceProvider provider) : BasePlugin {
base.Dispose(disposing);
}
}

View File

@@ -1,9 +1,13 @@
using System.Reactive.Concurrency;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SpecialRound;
using Stats;
using TTT.CS2;
using TTT.Game;
using TTT.Karma;
using TTT.RTD;
using TTT.Shop;
namespace TTT.Plugin;
@@ -16,5 +20,10 @@ public class TTTServiceCollection : IPluginServiceCollection<TTT> {
serviceCollection.AddGameServices();
serviceCollection.AddCS2Services();
serviceCollection.AddShopServices();
serviceCollection.AddRtdServices();
serviceCollection.AddSpecialRounds();
if (Environment.GetEnvironmentVariable("TTT_STATS_API_URL") == null) return;
serviceCollection.AddStatsServices();
}
}

107
TTT/RTD/AutoRTDCommand.cs Normal file
View File

@@ -0,0 +1,107 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Commands;
using JetBrains.Annotations;
using MAULActainShared.plugin.models;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Command;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.CS2.Command;
using TTT.CS2.ThirdParties.eGO;
using TTT.Game.Events.Game;
using TTT.Locale;
using TTT.RTD.lang;
namespace TTT.RTD;
public class AutoRTDCommand(IServiceProvider provider) : ICommand, IListener {
public string Id => "autortd";
private ICookie? autoRtdCookie;
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
private readonly ICommandManager commands =
provider.GetRequiredService<ICommandManager>();
private readonly IMsgLocalizer localizer =
provider.GetRequiredService<IMsgLocalizer>();
public bool MustBeOnMainThread => true;
public void Dispose() { }
public void Start() {
Task.Run(async () => {
var api = EgoApi.MAUL.Get();
if (api != null) {
await api.getCookieService().RegClientCookie("ttt_autortd");
autoRtdCookie =
await api.getCookieService().FindClientCookie("ttt_autortd");
}
});
}
public string[] RequiredFlags => ["@ttt/autortd"];
private Dictionary<string, bool> playerStatuses = new();
public async Task<CommandResult> Execute(IOnlinePlayer? executor,
ICommandInfo info) {
if (executor == null) return CommandResult.PLAYER_ONLY;
if (autoRtdCookie == null) {
info.ReplySync("AutoRTD system is not available.");
return CommandResult.SUCCESS;
}
if (!ulong.TryParse(executor.Id, out var executorId)) {
info.ReplySync("Your player ID is invalid for AutoRTD.");
return CommandResult.SUCCESS;
}
var value = await autoRtdCookie.Get(executorId);
if (value == "1") {
await autoRtdCookie.Set(executorId, "0");
info.ReplySync(localizer[RtdMsgs.COMMAND_AUTORTD_DISABLED]);
} else {
await autoRtdCookie.Set(executorId, "1");
info.ReplySync(localizer[RtdMsgs.COMMAND_AUTORTD_ENABLED]);
}
playerStatuses[executor.Id] = value != "1";
return CommandResult.SUCCESS;
}
[UsedImplicitly]
[EventHandler]
public void OnRoundStart(GameInitEvent ev) {
var messenger = provider.GetRequiredService<IMessenger>();
Task.Run(async () => {
foreach (var player in finder.GetOnline()) {
if (!playerStatuses.TryGetValue(player.Id, out var status)) {
await fetchCookie(player);
status = playerStatuses.GetValueOrDefault(player.Id, false);
}
if (!status) continue;
var info = new CS2CommandInfo(provider, player, 0, "css_rtd") {
CallingContext = CommandCallingContext.Chat
};
await Server.NextWorldUpdateAsync(() => commands.ProcessCommand(info));
}
});
}
private async Task fetchCookie(IPlayer player) {
if (autoRtdCookie == null) return;
if (!ulong.TryParse(player.Id, out var playerId)) return;
var value = await autoRtdCookie.Get(playerId);
playerStatuses[player.Id] = value == "1";
}
}

View File

@@ -0,0 +1,5 @@
namespace TTT.RTD;
public interface IRewardGenerator : IReadOnlyCollection<(IRtdReward, float)> {
IRtdReward GetReward();
}

10
TTT/RTD/IRtdReward.cs Normal file
View File

@@ -0,0 +1,10 @@
using TTT.API;
using TTT.API.Player;
namespace TTT.RTD;
public interface IRtdReward : ITerrorModule {
string Name { get; }
string Description { get; }
void GrantReward(IOnlinePlayer player);
}

5
TTT/RTD/Muted.cs Normal file
View File

@@ -0,0 +1,5 @@
using TTT.API.Player;
namespace TTT.RTD;
public class Muted : HashSet<string>, IMuted { }

24
TTT/RTD/RTD.csproj Normal file
View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>TTT.RTD</RootNamespace>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\API\API.csproj" />
<ProjectReference Include="..\CS2\CS2.csproj" />
<ProjectReference Include="..\Game\Game.csproj" />
<ProjectReference Include="..\ShopAPI\ShopAPI.csproj" />
<ProjectReference Include="..\Shop\Shop.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="MAULActainShared">
<HintPath>..\CS2\ThirdParties\Binaries\MAULActainShared.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,82 @@
using System.Collections;
using CounterStrikeSharp.API.Core;
using TTT.API;
using TTT.CS2.Items.Armor;
using TTT.CS2.Items.Camouflage;
using TTT.CS2.Items.Station;
using TTT.RTD.Rewards;
using TTT.Shop.Items;
using TTT.Shop.Items.Detective.Stickers;
using TTT.Shop.Items.Taser;
namespace TTT.RTD;
public class RewardGenerator(IServiceProvider provider)
: IRewardGenerator, IPluginModule {
private readonly List<(IRtdReward, float)> rewards = new();
private const float PROB_LOTTERY = 1 / 5000f;
private const float PROB_EXTREMELY_LOW = 1 / 800f;
private const float PROB_VERY_LOW = 1 / 100f;
private const float PROB_LOW = 1 / 20f;
private const float PROB_MEDIUM = 1 / 10f;
private const float PROB_OFTEN = 1 / 5f;
private const float PROB_VERY_OFTEN = 1 / 2f;
public IEnumerator<(IRtdReward, float)> GetEnumerator() {
return rewards.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
public int Count => rewards.Count;
public IRtdReward GetReward() {
var totalWeight = 0f;
foreach (var (_, weight) in rewards) { totalWeight += weight; }
var randomValue = Random.Shared.NextSingle() * totalWeight;
var cumulativeWeight = 0f;
foreach (var (reward, weight) in rewards) {
cumulativeWeight += weight;
if (randomValue <= cumulativeWeight) { return reward; }
}
return rewards[^1].Item1;
}
public void Start() {
rewards.AddRange([
(new CreditReward(provider, 5), PROB_OFTEN),
(new CreditReward(provider, -5), PROB_OFTEN),
(new WeaponReward(provider, "weapon_flashbang"), PROB_MEDIUM),
(new CreditReward(provider, -10), PROB_LOW),
(new HealthReward(provider, 150), PROB_LOW),
(new CreditReward(provider, -50), PROB_VERY_LOW),
(new CreditReward(provider, 50), PROB_VERY_LOW),
(new HealthReward(provider, 50), PROB_VERY_LOW),
(new ShopItemReward<CamouflageItem>(provider), PROB_VERY_LOW),
(new ShopItemReward<ArmorItem>(provider), PROB_VERY_LOW),
(new ShopItemReward<TaserItem>(provider), PROB_VERY_LOW),
(new ShopItemReward<Stickers>(provider), PROB_VERY_LOW),
(new ShopItemReward<OneShotDeagleItem>(provider), PROB_VERY_LOW),
(new ProvenReward(provider), PROB_VERY_LOW),
(new MuteReward(provider), PROB_VERY_LOW),
(new ShopItemReward<HealthStation>(provider), PROB_EXTREMELY_LOW),
(new HealthReward(provider, 1), PROB_EXTREMELY_LOW),
(new CreditReward(provider, 100), PROB_EXTREMELY_LOW),
(new HealthReward(provider, 200), PROB_EXTREMELY_LOW),
]);
rewards.ForEach(r => r.Item1.Start());
}
public void Start(BasePlugin? plugin) {
Start();
foreach (var (reward, _) in rewards) {
if (reward is IPluginModule module) { module.Start(plugin); }
}
}
public void Dispose() { }
}

View File

@@ -0,0 +1,22 @@
using CounterStrikeSharp.API;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Player;
using TTT.RTD.lang;
using TTT.Shop;
namespace TTT.RTD.Rewards;
public class CreditReward(IServiceProvider provider, int amo)
: RoundStartReward(provider) {
public override string Name => Locale[RtdMsgs.CREDITS_REWARD(amo)];
public override string Description
=> Locale[RtdMsgs.CREDITS_REWARD_DESC(amo)];
private readonly IShop shop = provider.GetRequiredService<IShop>();
public override void GiveOnRound(IOnlinePlayer player) {
shop.AddBalance(player, amo, "RTD Reward");
}
}

View File

@@ -0,0 +1,15 @@
using TTT.API.Player;
namespace TTT.RTD.Rewards;
public class HealthReward(IServiceProvider provider, int health)
: RoundStartReward(provider) {
public override string Name => $"{health} HP";
public override string Description
=> $"you will start next round with {health} HP";
public override void GiveOnRound(IOnlinePlayer player) {
player.Health = health;
}
}

View File

@@ -0,0 +1,50 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Game;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.Game.Events.Game;
using TTT.Locale;
using TTT.RTD.lang;
namespace TTT.RTD.Rewards;
public class MuteReward(IServiceProvider provider)
: RoundStartReward(provider), IPluginModule {
public override string Name => "Mute";
public override string Description
=> "you will not be able to communicate next round";
private readonly IMuted mutedPlayers = provider.GetRequiredService<IMuted>();
private readonly IMsgLocalizer locale =
provider.GetRequiredService<IMsgLocalizer>();
public override void GiveOnRound(IOnlinePlayer player) {
mutedPlayers.Add(player.Id);
}
public void Start(BasePlugin? plugin) {
plugin?.RegisterListener<Listeners.OnClientVoice>(onVoice);
}
private void onVoice(int playerSlot) {
var player = Utilities.GetPlayerFromSlot(playerSlot);
if (player == null) return;
if ((player.VoiceFlags & VoiceFlags.Muted) == VoiceFlags.Muted) return;
if (mutedPlayers.Contains(player.SteamID.ToString())) {
player.VoiceFlags |= VoiceFlags.Muted;
player.PrintToChat(locale[RtdMsgs.RTD_MUTED]);
}
}
public override void OnRoundStart(GameStateUpdateEvent ev) {
if (ev.NewState == State.FINISHED) mutedPlayers.Clear();
base.OnRoundStart(ev);
}
}

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