Compare commits

...

8 Commits

Author SHA1 Message Date
Michael Wilson
44e3f2240c chore: license updates (#199) 2023-12-14 11:03:18 +10:00
BuSheeZy
8af219e7a8 CHANGE: visual adjustments (#198) 2023-12-14 10:44:48 +10:00
Poggu
bff04e7795 Add voice manager (ability to override voice chat / mute players) (#179)
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
Co-authored-by: roflmuffin <shortguy014@gmail.com>
2023-12-12 17:46:45 +10:00
Michael Wilson
d495ac6230 chore: bump version 2023-12-12 10:55:55 +10:00
roflmuffin
f78abf0c81 fix: fallback to en language files for invariant mode 2023-12-12 10:51:38 +10:00
Charles_
bcacc42d0e feat: Added support for custom gamedata files. (#194) 2023-12-12 09:44:28 +10:00
HerrMagiic
8235d5e728 Update docs link in README.md (#193) 2023-12-11 23:11:03 +10:00
Michael Wilson
56739356d5 Plugin Translations (#146) 2023-12-11 16:19:50 +10:00
64 changed files with 1483 additions and 21 deletions

View File

@@ -22,6 +22,22 @@ saul/demofile-net, https://github.com/saul/demofile-net/blob/main/LICENSE:
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
neverlosecc/source2gen, https://github.com/neverlosecc/source2gen
source2gen - Source2 games SDK generator
Copyright 2023 neverlosecc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Source2ZE/CS2Fixes:
alliedmodders/sourcemod:
Source-Python-Dev-Team/Source.Python:

View File

@@ -75,6 +75,7 @@ SET(SOURCE_FILES
src/scripting/natives/natives_memory.cpp
src/scripting/natives/natives_schema.cpp
src/scripting/natives/natives_entities.cpp
src/scripting/natives/natives_voice.cpp
src/core/managers/entity_manager.cpp
src/core/managers/entity_manager.h
src/core/managers/chat_manager.cpp
@@ -83,6 +84,8 @@ SET(SOURCE_FILES
src/core/managers/server_manager.h
src/scripting/natives/natives_server.cpp
libraries/nlohmann/json.hpp
src/core/managers/voice_manager.cpp
src/core/managers/voice_manager.h
src/scripting/natives/natives_dynamichooks.cpp
)

View File

@@ -14,7 +14,7 @@ Due to the architectural changes of CS2, the plugin is being rebuilt on the grou
Download the latest build from [here](https://github.com/roflmuffin/CounterStrikeSharp/releases). (Download the with runtime version if this is your first time installing).
Detailed installation instructions can be found in the [docs](https://docs.cssharp.dev/guides/getting-started/).
Detailed installation instructions can be found in the [docs](https://docs.cssharp.dev/docs/guides/getting-started.html).
## What works?
@@ -128,3 +128,4 @@ Build
```bash
cmake --build . --config Debug
```

View File

@@ -2,5 +2,6 @@
"PublicChatTrigger": [ "!" ],
"SilentChatTrigger": [ "/" ],
"FollowCS2ServerGuidelines": true,
"PluginHotReloadEnabled": true
"PluginHotReloadEnabled": true,
"ServerLanguage": "en"
}

View File

@@ -32,3 +32,7 @@ receive a ban.
## PluginHotReloadEnabled
When enabled, plugins are automatically reloaded when their .dll file is updated.
## ServerLanguage
Configures the default language to use for server commands & messages. The format for the culture name based on RFC 4646 is `languagecode2-country`/`regioncode2`, where `languagecode2` is the two-letter language code and `country/regioncode2` is the two-letter subculture code. Examples include `ja-JP` for Japanese (Japan) and `en-US` for English (United States). Defaults to "en".

View File

@@ -0,0 +1,5 @@
[!INCLUDE [WithTranslations](../../examples/WithTranslations/README.md)]
<a href="https://github.com/roflmuffin/CounterStrikeSharp/tree/main/examples/WithTranslations" class="btn btn-secondary">View project on Github <i class="bi bi-github"></i></a>
[!code-csharp[](../../examples/WithTranslations/WithTranslationsPlugin.cs)]

View File

@@ -0,0 +1,5 @@
[!INCLUDE [WithVoiceOverrides](../../examples/WithVoiceOverrides/README.md)]
<a href="https://github.com/roflmuffin/CounterStrikeSharp/tree/main/examples/WithVoiceOverrides" class="btn btn-secondary">View project on Github <i class="bi bi-github"></i></a>
[!code-csharp[](../../examples/WithVoiceOverrides/WithVoiceOverridesPlugin.cs)]

View File

@@ -13,5 +13,9 @@ items:
href: WithGameEventHandlers.md
- name: Database (Dapper)
href: WithDatabase.md
- name: Translations
href: WithTranslations.md
- name: Voice Overrides
href: WithVoiceOverrides.md
- name: Warcraft Plugin
href: WarcraftPlugin.md

View File

@@ -14,7 +14,7 @@ description: Write Counter-Strike 2 server plugins in C#.
<span>CounterStrikeSharp is a simpler way to write CS2 server plugins.</span>
<div>
<a href="docs/guides/getting-started.md" class="btn btn-primary btn-lg fw-bold my-5">Get Started <i class="bi bi-arrow-right"></a>
<a href="https://github.com/roflmuffin/CounterStrikeSharp/releases/latest" class="btn btn-primary btn-lg fw-bold my-5">Download <i class="bi bi-arrow-right"></a>
<a href="https://github.com/roflmuffin/CounterStrikeSharp/releases/latest" class="btn btn-secondary btn-lg fw-bold my-5">Download <i class="bi bi-download"></a>
</div>
</div>

View File

@@ -0,0 +1,8 @@
# With Translations
This example shows how to use an `IStringLocalizer` and language `json` files to provide localization for your plugins.
## How to use
1. Add the `IStringLocalizer` to your services with dependency injection, or use the `Localizer` provided on the plugin instance.
2. Add a `lang` folder to your plugin, and add a `json` file for each language you want to support. The name of the file should be a locale code, like `en.json` or `fr.json` etc.
3. Ensure that the `lang` folder is shipped with your plugin, see the example `.csproj` file for an example to auto-copy to the output folder.
4. Use the `IStringLocalizer` to localize your strings.

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\managed\CounterStrikeSharp.API\CounterStrikeSharp.API.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="lang\**\*.*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,52 @@
using System.Globalization;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Commands;
using Microsoft.Extensions.Logging;
namespace WithTranslations;
[MinimumApiVersion(80)]
public class WithTranslationsPlugin : BasePlugin
{
public override string ModuleName => "Example: With Translations";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "CounterStrikeSharp & Contributors";
public override string ModuleDescription => "A simple plugin that provides translations";
public override void Load(bool hotReload)
{
// A global `Localizer` is provided on the plugin instance.
// You can also use dependency injection to inject `IStringLocalizer` in your own services.
Logger.LogInformation("This message is in the server language: {Message}", Localizer["test.translation"]);
// IStringLocalizer can accept standard string format arguments.
// "This number has 2 decimal places {0:n2}" -> "This number has 2 decimal places 123.55"
Logger.LogInformation(Localizer["test.format", 123.551]);
// This message has colour codes
Server.PrintToChatAll(Localizer["test.colors"]);
// This message has colour codes and formatted values
Server.PrintToChatAll(Localizer["test.colors.withformat", 123.551]);
}
[ConsoleCommand("css_replylanguage", "Test Translations")]
public void OnCommandReplyLanguage(CCSPlayerController? player, CommandInfo command)
{
// Commands are executed in a players provided culture (or fallback to server culture).
// Players can configure their language using the `!lang` or `css_lang` command.
Logger.LogInformation("Current Culture is {Culture}", CultureInfo.CurrentCulture);
command.ReplyToCommand(Localizer["test.translation"]);
if (player != null)
{
// You can also get the players language using the `GetLanguage` extension method.
// This will always return a culture, defaulting to the server culture if the user has not configured it.
var language = player.GetLanguage();
}
}
}

View File

@@ -0,0 +1,3 @@
{
"test.translation": "This is the english (GB) translation"
}

View File

@@ -0,0 +1,6 @@
{
"test.translation": "This is the english translation",
"test.format": "This number has 2 decimal places {0:n2}",
"test.colors": "{orange}This{default} text has {green}green{default} text",
"test.colors.withformat": "{orange}{0:n2}{default}"
}

View File

@@ -0,0 +1,3 @@
{
"test.translation": "This is the french translation"
}

View File

@@ -0,0 +1,2 @@
# With Voice Overrides
Provides examples how to manipulate player voice flags & listening overrides to prevent certain players from hearing others.

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\managed\CounterStrikeSharp.API\CounterStrikeSharp.API.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,79 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Commands;
using Microsoft.Extensions.Logging;
namespace WithVoiceOverrides;
[MinimumApiVersion(80)]
public class WithVoiceOverridesPlugin : BasePlugin
{
public override string ModuleName => "Example: With Voice Overrides";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "CounterStrikeSharp & Contributors";
public override string ModuleDescription => "A plugin that manipulates voice flags";
[ConsoleCommand("css_hearall")]
public void OnHearAllCommand(CCSPlayerController? caller, CommandInfo command)
{
if (caller is null) return;
if (caller.VoiceFlags.HasFlag(VoiceFlags.ListenAll))
{
caller.VoiceFlags = VoiceFlags.Normal;
command.ReplyToCommand("Voice set back to default");
}
else
{
caller.VoiceFlags = VoiceFlags.ListenAll;
command.ReplyToCommand("Can hear both teams");
}
}
[ConsoleCommand("css_muteself")]
public void OnMuteSelfCommand(CCSPlayerController? caller, CommandInfo command)
{
if (caller is null) return;
if (caller.VoiceFlags.HasFlag(VoiceFlags.Muted))
{
caller.VoiceFlags = VoiceFlags.Normal;
command.ReplyToCommand("Unmuted yourself");
}
else
{
caller.VoiceFlags = VoiceFlags.Muted;
command.ReplyToCommand("Muted yourself");
}
}
[ConsoleCommand("css_muteothers")]
[CommandHelper(minArgs: 1, usage: "[target]")]
public void OnMuteOthersCommand(CCSPlayerController? caller, CommandInfo command)
{
if (caller is null) return;
var targetResult = command.GetArgTargetResult(1);
foreach (var player in targetResult.Players)
{
if (player == caller) continue;
var existingOverride = caller.GetListenOverride(player);
if (existingOverride == ListenOverride.Mute)
{
caller.SetListenOverride(player, ListenOverride.Default);
command.ReplyToCommand($"Now hearing {player.PlayerName}");
}
else
{
caller.SetListenOverride(player, ListenOverride.Mute);
command.ReplyToCommand($"Muted {player.PlayerName}");
}
}
}
}

View File

@@ -10,6 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2"/>
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -29,4 +30,16 @@
<ProjectReference Include="..\CounterStrikeSharp.API\CounterStrikeSharp.API.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="lang\en.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="lang\en-GB.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="lang\fr.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,3 @@
{
"test.translation": "This is the english (GB) translation"
}

View File

@@ -0,0 +1,6 @@
{
"test.translation": "This is the english translation",
"test.format": "This number has 2 decimal places {0:n2}",
"test.colors": "{orange}This{default} text has {green}green{default} text",
"test.colors.withformat": "{orange}{0:n2}{default}"
}

View File

@@ -0,0 +1,3 @@
{
"test.translation": "This is the french translation"
}

View File

@@ -0,0 +1,92 @@
using System.Globalization;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Plugin;
using CounterStrikeSharp.API.Core.Translations;
using Microsoft.Extensions.Localization;
using Moq;
namespace CounterStrikeSharp.API.Tests;
public class TranslationTests
{
private readonly JsonStringLocalizerFactory _factory;
private readonly IStringLocalizer _localizer;
public TranslationTests()
{
var pluginContextMock = new Mock<IPluginContext>();
pluginContextMock.SetupGet(x => x.FilePath).Returns(TestUtils.GetTestPath("test_plugin.dll"));
_factory = new JsonStringLocalizerFactory(pluginContextMock.Object);
_localizer = _factory.Create(this.GetType());
// This is generally derived from the core config, but for the sake of these tests we default to `en`.
var serverCulture = CultureInfo.GetCultureInfo("en");
CultureInfo.DefaultThreadCurrentUICulture = serverCulture;
CultureInfo.DefaultThreadCurrentCulture = serverCulture;
CultureInfo.CurrentUICulture = serverCulture;
CultureInfo.CurrentCulture = serverCulture;
}
[Fact]
public void TranslatesLanguagesCorrectly()
{
using (new WithTemporaryCulture(CultureInfo.GetCultureInfo("en")))
{
Assert.Equal("This is the english translation", _localizer["test.translation"]);
}
using (new WithTemporaryCulture(CultureInfo.InvariantCulture))
{
Assert.Equal("This is the english translation", _localizer["test.translation"]);
}
using (new WithTemporaryCulture(CultureInfo.GetCultureInfo("en-US")))
{
Assert.Equal("This is the english translation", _localizer["test.translation"]);
}
using (new WithTemporaryCulture(CultureInfo.GetCultureInfo("en-GB")))
{
Assert.Equal("This is the english (GB) translation", _localizer["test.translation"]);
}
using (new WithTemporaryCulture(CultureInfo.GetCultureInfo("fr")))
{
Assert.Equal("This is the french translation", _localizer["test.translation"]);
}
using (new WithTemporaryCulture(CultureInfo.GetCultureInfo("fr-FR")))
{
Assert.Equal("This is the french translation", _localizer["test.translation"]);
}
}
[Fact]
public void ShouldFallbackToServerLanguage()
{
using (new WithTemporaryCulture(CultureInfo.GetCultureInfo("de")))
{
Assert.Equal("This is the english translation", _localizer["test.translation"]);
}
}
[Fact]
public void ShouldReturnKeyIfNotFound()
{
Assert.Equal("test.notfound", _localizer["test.notfound"]);
}
[Fact]
public void ShouldHandleFormatStrings()
{
Assert.Equal("This number has 2 decimal places 0.25", _localizer["test.format", 0.251]);
}
[Fact]
public void HandlesColorFormatting()
{
// Sets invisible pre-color if there is a color code in the string.
Assert.Equal(" \x10This\x01 text has \x04green\x01 text", _localizer["test.colors"]);
Assert.Equal($" {'\x10'}1.25\x01", _localizer["test.colors.withformat", 1.25]);
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
@@ -7,6 +8,7 @@ using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Logging;
using CounterStrikeSharp.API.Core.Plugin;
using CounterStrikeSharp.API.Core.Plugin.Host;
using CounterStrikeSharp.API.Core.Translations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -38,6 +40,7 @@ public static class Bootstrap
services.AddSingleton<IScriptHostConfiguration, ScriptHostConfiguration>();
services.AddScoped<Application>();
services.AddSingleton<IPluginManager, PluginManager>();
services.AddSingleton<IPlayerLanguageManager, PlayerLanguageManager>();
services.AddScoped<IPluginContextQueryHandler, PluginContextQueryHandler>();
services.Scan(i => i.FromCallingAssembly()

View File

@@ -1311,5 +1311,51 @@ namespace CounterStrikeSharp.API.Core
return (bool)ScriptContext.GlobalScriptContext.GetResult(typeof(bool));
}
}
public static void SetClientListening(IntPtr receiver, IntPtr sender, uint listen){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(receiver);
ScriptContext.GlobalScriptContext.Push(sender);
ScriptContext.GlobalScriptContext.Push(listen);
ScriptContext.GlobalScriptContext.SetIdentifier(0xD38BEE77);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
}
}
public static ListenOverride GetClientListening(IntPtr receiver, IntPtr sender){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(receiver);
ScriptContext.GlobalScriptContext.Push(sender);
ScriptContext.GlobalScriptContext.SetIdentifier(0xE95644E3);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
return (ListenOverride)ScriptContext.GlobalScriptContext.GetResult(typeof(ListenOverride));
}
}
public static void SetClientVoiceFlags(IntPtr client, uint flags){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(client);
ScriptContext.GlobalScriptContext.Push(flags);
ScriptContext.GlobalScriptContext.SetIdentifier(0x48EB2FC8);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
}
}
public static uint GetClientVoiceFlags(IntPtr client){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(client);
ScriptContext.GlobalScriptContext.SetIdentifier(0x9685205C);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
return (uint)ScriptContext.GlobalScriptContext.GetResult(typeof(uint));
}
}
}
}

View File

@@ -14,13 +14,16 @@
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
using System.Globalization;
using System.Linq;
using System.Text;
using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Plugin;
using CounterStrikeSharp.API.Core.Plugin.Host;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.Logging;
@@ -40,10 +43,11 @@ namespace CounterStrikeSharp.API.Core
private readonly CoreConfig _coreConfig;
private readonly IPluginManager _pluginManager;
private readonly IPluginContextQueryHandler _pluginContextQueryHandler;
private readonly IPlayerLanguageManager _playerLanguageManager;
public Application(ILoggerFactory loggerFactory, IScriptHostConfiguration scriptHostConfiguration,
GameDataProvider gameDataProvider, CoreConfig coreConfig, IPluginManager pluginManager,
IPluginContextQueryHandler pluginContextQueryHandler)
IPluginContextQueryHandler pluginContextQueryHandler, IPlayerLanguageManager playerLanguageManager)
{
Logger = loggerFactory.CreateLogger("Core");
_scriptHostConfiguration = scriptHostConfiguration;
@@ -51,6 +55,7 @@ namespace CounterStrikeSharp.API.Core
_coreConfig = coreConfig;
_pluginManager = pluginManager;
_pluginContextQueryHandler = pluginContextQueryHandler;
_playerLanguageManager = playerLanguageManager;
_instance = this;
}
@@ -99,7 +104,7 @@ namespace CounterStrikeSharp.API.Core
info.ReplyToCommand(
" CounterStrikeSharp was created and is maintained by Michael \"roflmuffin\" Wilson.\n" +
" Counter-Strike Sharp uses code borrowed from SourceMod, Source.Python, FiveM, Saul Rennison and CS2Fixes.\n" +
" Counter-Strike Sharp uses code borrowed from SourceMod, Source.Python, FiveM, Saul Rennison, source2gen and CS2Fixes.\n" +
" See ACKNOWLEDGEMENTS.md for more information.\n" +
" Current API Version: " + currentVersion, true);
return;
@@ -153,7 +158,7 @@ namespace CounterStrikeSharp.API.Core
true);
break;
}
// If our arugment doesn't end in ".dll" - try and construct a path similar to PluginName/PluginName.dll.
// We'll assume we have a full path if we have ".dll".
var path = info.GetArg(2);
@@ -173,7 +178,8 @@ namespace CounterStrikeSharp.API.Core
try
{
_pluginManager.LoadPlugin(path);
} catch (Exception e)
}
catch (Exception e)
{
info.ReplyToCommand($"Could not load plugin \"{path}\")", true);
}
@@ -182,7 +188,7 @@ namespace CounterStrikeSharp.API.Core
{
plugin.Load(false);
}
break;
}
@@ -245,11 +251,44 @@ namespace CounterStrikeSharp.API.Core
}
}
[CommandHelper(usage: "[language code, e.g. \"de\", \"pl\", \"en\"]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
private void OnLangCommand(CCSPlayerController? player, CommandInfo command)
{
if (player == null) return;
SteamID steamId = (SteamID)player.SteamID;
if (command.ArgCount == 1)
{
var language = _playerLanguageManager.GetLanguage(steamId);
command.ReplyToCommand(string.Format("Current language is \"{0}\" ({1})", language.Name, language.NativeName));
return;
}
if (command.ArgCount != 2)
{
return;
}
try
{
var language = command.GetArg(1);
var cultureInfo = CultureInfo.GetCultures(CultureTypes.AllCultures).Single(x => x.Name == language);
_playerLanguageManager.SetLanguage(steamId, cultureInfo);
command.ReplyToCommand($"Language set to {cultureInfo.NativeName}");
}
catch (InvalidOperationException)
{
command.ReplyToCommand("Language not found.");
}
}
private void RegisterPluginCommands()
{
CommandUtils.AddStandaloneCommand("css", "Counter-Strike Sharp options.", OnCSSCommand);
CommandUtils.AddStandaloneCommand("css_plugins", "Counter-Strike Sharp plugin options.",
OnCSSPluginCommand);
CommandUtils.AddStandaloneCommand("css_lang", "Set Counter-Strike Sharp language", OnLangCommand);
}
}
}

View File

@@ -21,12 +21,14 @@ using System.Linq;
using System.Reflection;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Events;
using CounterStrikeSharp.API.Modules.Timers;
using CounterStrikeSharp.API.Modules.Config;
using CounterStrikeSharp.API.Modules.Entities;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
@@ -58,6 +60,8 @@ namespace CounterStrikeSharp.API.Core
public string ModuleDirectory => Path.GetDirectoryName(ModulePath);
public ILogger Logger { get; set; }
public IStringLocalizer Localizer { get; set; }
public virtual void Load(bool hotReload)
{
}
@@ -170,6 +174,8 @@ namespace CounterStrikeSharp.API.Core
{
var caller = (i != -1) ? new CCSPlayerController(NativeAPI.GetEntityFromIndex(i + 1)) : null;
var command = new CommandInfo(ptr, caller);
using var temporaryCulture = new WithTemporaryCulture(caller.GetLanguage());
var methodInfo = handler?.GetMethodInfo();

View File

@@ -1,4 +1,4 @@
/*
/*
* This file is part of CounterStrikeSharp.
* CounterStrikeSharp is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,6 +23,8 @@ using CounterStrikeSharp.API.Modules.Utils;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Logging;
using Microsoft.Extensions.Hosting;
@@ -46,6 +48,9 @@ namespace CounterStrikeSharp.API.Core
[JsonPropertyName("PluginHotReloadEnabled")]
public bool PluginHotReloadEnabled { get; set; } = true;
[JsonPropertyName("ServerLanguage")]
public string ServerLanguage { get; set; } = "en";
}
/// <summary>
@@ -88,6 +93,8 @@ namespace CounterStrikeSharp.API.Core
/// When enabled, plugins are automatically reloaded when their .dll file is updated.
/// </summary>
public static bool PluginHotReloadEnabled => _coreConfig.PluginHotReloadEnabled;
public static string ServerLanguage => _coreConfig.ServerLanguage;
}
public partial class CoreConfig : IStartupService
@@ -138,6 +145,29 @@ namespace CounterStrikeSharp.API.Core
{
_logger.LogWarning(ex, "Failed to load core configuration, fallback values will be used");
}
var serverCulture = CultureInfo.GetCultures(CultureTypes.AllCultures)
.FirstOrDefault(x => x.Name == ServerLanguage);
if (serverCulture == null)
{
try
{
_logger.LogWarning("Server Language \"{ServerLanguage}\" is not supported, falling back to \"en\"",
ServerLanguage);
_coreConfig.ServerLanguage = "en";
serverCulture = new CultureInfo("en");
}
catch (Exception)
{
_logger.LogWarning("Server is running in invariant mode, translations will not be available.");
serverCulture = CultureInfo.InvariantCulture;
}
}
CultureInfo.DefaultThreadCurrentUICulture = serverCulture;
CultureInfo.DefaultThreadCurrentCulture = serverCulture;
CultureInfo.CurrentUICulture = serverCulture;
CultureInfo.CurrentCulture = serverCulture;
_logger.LogInformation("Successfully loaded core configuration");
}

View File

@@ -34,34 +34,58 @@ public class Offsets
public sealed class GameDataProvider : IStartupService
{
private readonly string _gameDataFilePath;
private readonly string _gameDataDirectoryPath;
public Dictionary<string,LoadedGameData> Methods;
private readonly ILogger<GameDataProvider> _logger;
public GameDataProvider(IScriptHostConfiguration scriptHostConfiguration, ILogger<GameDataProvider> logger)
{
_logger = logger;
_gameDataFilePath = Path.Join(scriptHostConfiguration.GameDataPath, "gamedata.json");
_gameDataDirectoryPath = scriptHostConfiguration.GameDataPath;
}
public void Load()
{
try
{
Methods = JsonSerializer.Deserialize<Dictionary<string, LoadedGameData>>(File.ReadAllText(_gameDataFilePath))!;
Methods = new Dictionary<string, LoadedGameData>();
foreach (string filePath in Directory.EnumerateFiles(_gameDataDirectoryPath, "*.json"))
{
string jsonContent = File.ReadAllText(filePath);
Dictionary<string, LoadedGameData> loadedMethods = JsonSerializer.Deserialize<Dictionary<string, LoadedGameData>>(jsonContent)!;
foreach (KeyValuePair<string, LoadedGameData> loadedMethod in loadedMethods)
{
if (Methods.ContainsKey(loadedMethod.Key))
{
_logger.LogWarning("GameData Method \"{Key}\" loaded a duplicate entry from {filePath}.", loadedMethod.Key, filePath);
}
Methods[loadedMethod.Key] = loadedMethod.Value;
}
if (loadedMethods != null)
{
_logger.LogInformation("Successfully loaded {Count} game data entries from {Path}", loadedMethods.Count, filePath);
}
else
{
_logger.LogWarning("Unable to load game data entries from {Path}, game data file is empty", filePath);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load game data");
}
_logger.LogInformation("Successfully loaded {Count} game data entries from {Path}", Methods.Count, _gameDataFilePath);
}
}
public static class GameData
{
internal static GameDataProvider GameDataProvider { get; set; } = null!;
public static string GetSignature(string key)
{
Application.Instance.Logger.LogDebug("Getting signature: {Key}", key);

View File

@@ -16,6 +16,7 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
@@ -54,6 +55,8 @@ namespace CounterStrikeSharp.API.Core
string ModulePath { get; internal set; }
ILogger Logger { get; set; }
IStringLocalizer Localizer { get; set; }
void RegisterAllAttributes(object instance);

View File

@@ -202,6 +202,21 @@ public partial class CCSPlayerController
public void ExecuteClientCommand(string command) => NativeAPI.IssueClientCommand(Slot, command);
/// <summary>
/// Overrides who a player can hear in voice chat.
/// </summary>
/// <param name="sender">Player talking in the voice chat</param>
/// <param name="override">Whether the talker should be heard</param>
public void SetListenOverride(CCSPlayerController sender, ListenOverride @override)
{
NativeAPI.SetClientListening(Handle, sender.Handle, (Byte)@override);
}
public ListenOverride GetListenOverride(CCSPlayerController sender)
{
return NativeAPI.GetClientListening(Handle, sender.Handle);
}
public int Slot => (int)Index - 1;
/// <summary>
@@ -234,4 +249,16 @@ public partial class CCSPlayerController
return ipAddress;
}
}
/// <summary>
/// Determines how the player interacts with voice chat.
/// </summary>
public VoiceFlags VoiceFlags
{
get => (VoiceFlags)NativeAPI.GetClientVoiceFlags(Handle);
set
{
NativeAPI.SetClientVoiceFlags(Handle, (Byte)value);
}
}
}

View File

@@ -6,6 +6,7 @@ public interface IPluginContext
IPlugin Plugin { get; }
int PluginId { get; }
string FilePath { get; }
void Load(bool hotReload);
void Unload(bool hotReload);
}

View File

@@ -20,8 +20,11 @@ using System.Threading.Tasks;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Logging;
using CounterStrikeSharp.API.Core.Translations;
using McMaster.NETCore.Plugins;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@@ -43,6 +46,8 @@ namespace CounterStrikeSharp.API.Core.Plugin
private readonly string _path;
private readonly FileSystemWatcher _fileWatcher;
private readonly IServiceProvider _applicationServiceProvider;
public string FilePath => _path;
// TOOD: ServiceCollection
private ILogger _logger = CoreLogging.Factory.CreateLogger<PluginContext>();
@@ -163,8 +168,12 @@ namespace CounterStrikeSharp.API.Core.Plugin
method?.Invoke(pluginServiceCollection, new object[] { serviceCollection });
}
}
serviceCollection.AddSingleton<IPluginContext>(this);
serviceCollection.TryAddSingleton<IStringLocalizerFactory, JsonStringLocalizerFactory>();
serviceCollection.TryAddTransient(typeof(IStringLocalizer<>), typeof(StringLocalizer<>));
serviceCollection.TryAddTransient(typeof(IStringLocalizer), typeof(StringLocalizer));
serviceCollection.AddSingleton(this);
ServiceProvider = serviceCollection.BuildServiceProvider();
var minimumApiVersion = pluginType.GetCustomAttribute<MinimumApiVersion>()?.Version;
@@ -185,6 +194,7 @@ namespace CounterStrikeSharp.API.Core.Plugin
Plugin.ModulePath = _path;
Plugin.RegisterAllAttributes(Plugin);
Plugin.Localizer = ServiceProvider.GetRequiredService<IStringLocalizer>();
Plugin.Logger = ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(pluginType);
Plugin.InitializeConfig(Plugin, pluginType);

View File

@@ -0,0 +1,11 @@
using System.Globalization;
using CounterStrikeSharp.API.Modules.Entities;
namespace CounterStrikeSharp.API.Core.Translations;
public interface IPlayerLanguageManager
{
void SetLanguage(SteamID steamId, CultureInfo cultureInfo);
CultureInfo GetLanguage(SteamID steamId);
CultureInfo GetDefaultLanguage();
}

View File

@@ -0,0 +1,153 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
namespace CounterStrikeSharp.API.Core.Translations
{
public class JsonResourceManager
{
private static readonly JsonDocumentOptions _jsonDocumentOptions = new()
{
CommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
};
private ConcurrentDictionary<string, ConcurrentDictionary<string, string>> _resourcesCache = new();
public JsonResourceManager(string resourcesPath)
{
ResourcesPath = resourcesPath;
}
public string ResourcesPath { get; }
public virtual ConcurrentDictionary<string, string> GetResourceSet(string cultureName)
{
TryLoadResourceSet(cultureName);
_resourcesCache.TryGetValue(cultureName, out ConcurrentDictionary<string, string> resources);
return resources;
}
public virtual ConcurrentDictionary<string, string> GetResourceSet(CultureInfo culture, bool tryParents)
{
TryLoadResourceSet(culture);
if (tryParents)
{
var allResources = new ConcurrentDictionary<string, string>();
do
{
TryLoadResourceSet(culture);
if (_resourcesCache.TryGetValue(culture.Name, out ConcurrentDictionary<string, string> resources))
{
foreach (var entry in resources)
{
allResources.TryAdd(entry.Key, entry.Value);
}
}
culture = culture.Parent;
} while (culture != CultureInfo.InvariantCulture);
return allResources;
}
else
{
_resourcesCache.TryGetValue(culture.Name, out ConcurrentDictionary<string, string> resources);
return resources;
}
}
public string GetFallbackString(string name)
{
GetResourceSet("en");
if (_resourcesCache.ContainsKey("en"))
{
if (_resourcesCache["en"].TryGetValue(name, out string value))
{
return value;
}
}
return null;
}
public virtual string GetString(string name)
{
var culture = CultureInfo.CurrentUICulture;
GetResourceSet(culture, tryParents: true);
if (_resourcesCache.Count == 0)
{
return null;
}
do
{
if (_resourcesCache.ContainsKey(culture.Name))
{
if (_resourcesCache[culture.Name].TryGetValue(name, out string value))
{
return value;
}
}
culture = culture.Parent;
} while (culture != culture.Parent);
return null;
}
public virtual string? GetString(string name, CultureInfo culture)
{
var values = GetResourceSet(culture, tryParents: true);
if (values.Count == 0)
{
return null;
}
return values.TryGetValue(name, out string value)
? value
: null;
}
private void TryLoadResourceSet(string cultureName)
{
if (!_resourcesCache.ContainsKey(cultureName))
{
var file = Path.Combine(ResourcesPath, $"{cultureName}.json");
var resources = LoadJsonResources(file);
_resourcesCache.TryAdd(cultureName,
new ConcurrentDictionary<string, string>(resources.ToDictionary(r => r.Key, r => r.Value)));
}
}
private void TryLoadResourceSet(CultureInfo culture)
{
TryLoadResourceSet(culture.Name);
}
private static IDictionary<string, string> LoadJsonResources(string filePath)
{
var resources = new Dictionary<string, string>();
if (File.Exists(filePath))
{
using var reader = new StreamReader(filePath);
using var document = JsonDocument.Parse(reader.BaseStream, _jsonDocumentOptions);
resources = document.RootElement.EnumerateObject().ToDictionary(e => e.Name, e => e.Value.ToString());
}
return resources;
}
}
}

View File

@@ -0,0 +1,122 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Resources;
using Microsoft.Extensions.Localization;
namespace CounterStrikeSharp.API.Core.Translations;
public class JsonStringLocalizer : IStringLocalizer
{
private readonly JsonResourceManager _resourceManager;
private readonly JsonStringProvider _resourceStringProvider;
public JsonStringLocalizer(string langPath)
{
_resourceManager = new JsonResourceManager(langPath);
_resourceStringProvider = new(_resourceManager);
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{
return GetAllStrings(includeParentCultures, CultureInfo.CurrentUICulture);
}
public LocalizedString this[string name]
{
get
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
var value = GetStringSafely(name);
return new LocalizedString(name, value ?? name, resourceNotFound: value == null);
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
var format = GetStringSafely(name);
var value = string.Format(format ?? name, arguments);
return new LocalizedString(name, value, resourceNotFound: format == null);
}
}
protected string? GetStringSafely(string name, CultureInfo? culture = null)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
culture = culture ?? CultureInfo.CurrentUICulture;
var result = _resourceManager.GetString(name, culture);
// Fallback to en if running in invariant mode.
if (result == null && culture.Equals(CultureInfo.InvariantCulture))
{
result = _resourceManager.GetFallbackString(name);
}
// Fallback to the default culture (en-US) if the resource is not found for the current culture.
if (result == null && !culture.Equals(CultureInfo.DefaultThreadCurrentUICulture))
{
result = _resourceManager.GetString(name, CultureInfo.DefaultThreadCurrentUICulture!);
}
return result?.ReplaceColorTags();
}
protected virtual IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures, CultureInfo culture)
{
if (culture == null)
{
throw new ArgumentNullException(nameof(culture));
}
var resourceNames = includeParentCultures
? GetResourceNamesFromCultureHierarchy(culture)
: _resourceStringProvider.GetAllResourceStrings(culture, true);
foreach (var name in resourceNames)
{
var value = GetStringSafely(name, culture);
yield return new LocalizedString(name, value ?? name, resourceNotFound: value == null);
}
}
private IEnumerable<string> GetResourceNamesFromCultureHierarchy(CultureInfo startingCulture)
{
var currentCulture = startingCulture;
var resourceNames = new HashSet<string>();
while (currentCulture != currentCulture.Parent)
{
var cultureResourceNames = _resourceStringProvider.GetAllResourceStrings(currentCulture, false);
if (cultureResourceNames != null)
{
foreach (var resourceName in cultureResourceNames)
{
resourceNames.Add(resourceName);
}
}
currentCulture = currentCulture.Parent;
}
return resourceNames;
}
}

View File

@@ -0,0 +1,25 @@
using CounterStrikeSharp.API.Core.Hosting;
using CounterStrikeSharp.API.Core.Plugin;
using Microsoft.Extensions.Localization;
namespace CounterStrikeSharp.API.Core.Translations;
public class JsonStringLocalizerFactory : IStringLocalizerFactory
{
private readonly IPluginContext _pluginContext;
public JsonStringLocalizerFactory(IPluginContext pluginContext)
{
_pluginContext = pluginContext;
}
public IStringLocalizer Create(Type resourceSource)
{
return new JsonStringLocalizer(Path.Join(Path.GetDirectoryName(_pluginContext.FilePath), "lang"));
}
public IStringLocalizer Create(string baseName, string location)
{
return new JsonStringLocalizer(Path.Join(Path.GetDirectoryName(_pluginContext.FilePath), "lang"));
}
}

View File

@@ -0,0 +1,51 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Resources;
namespace CounterStrikeSharp.API.Core.Translations;
public class JsonStringProvider
{
private readonly ConcurrentDictionary<string, IList<string>> _resourceNamesCache = new();
private readonly JsonResourceManager _jsonResourceManager;
public JsonStringProvider(JsonResourceManager jsonResourceManager)
{
_jsonResourceManager = jsonResourceManager;
}
private string GetResourceCacheKey(CultureInfo culture)
{
return $"Culture={culture.Name}";
}
public IList<string> GetAllResourceStrings(CultureInfo culture, bool throwOnMissing)
{
var cacheKey = GetResourceCacheKey(culture);
return _resourceNamesCache.GetOrAdd(cacheKey, _ =>
{
var resourceSet = _jsonResourceManager.GetResourceSet(culture, tryParents: false);
if (resourceSet == null)
{
if (throwOnMissing)
{
throw new MissingManifestResourceException($"The manifest resource for the culture '{culture.Name}' is missing.");
}
else
{
return null;
}
}
var names = new List<string>();
foreach (var entry in resourceSet)
{
names.Add(entry.Key);
}
return names;
});
}
}

View File

@@ -0,0 +1,17 @@
using System.Globalization;
using CounterStrikeSharp.API.Modules.Entities;
namespace CounterStrikeSharp.API.Core.Translations;
public static class PlayerLanguageExtensions
{
/// <summary>
/// Returns the players configured language, as set using the "css_lang" command.
/// </summary>
public static CultureInfo GetLanguage(this CCSPlayerController? player)
{
if (player == null || !player.IsValid) return PlayerLanguageManager.Instance.GetDefaultLanguage();
return PlayerLanguageManager.Instance.GetLanguage((SteamID)player.SteamID);
}
}

View File

@@ -0,0 +1,32 @@
using System.Collections.Concurrent;
using System.Globalization;
using CounterStrikeSharp.API.Modules.Entities;
namespace CounterStrikeSharp.API.Core.Translations;
public class PlayerLanguageManager : IPlayerLanguageManager
{
private readonly ConcurrentDictionary<SteamID, CultureInfo> _playerLanguages = new();
public static IPlayerLanguageManager Instance { get; private set; } = null!;
public PlayerLanguageManager()
{
Instance = this;
}
public void SetLanguage(SteamID steamId, CultureInfo cultureInfo)
{
_playerLanguages[steamId] = cultureInfo;
}
public CultureInfo GetLanguage(SteamID steamId)
{
return _playerLanguages.TryGetValue(steamId, out var cultureInfo) ? cultureInfo : GetDefaultLanguage();
}
public CultureInfo GetDefaultLanguage()
{
return CultureInfo.DefaultThreadCurrentUICulture!;
}
}

View File

@@ -0,0 +1,22 @@
using System.Reflection;
using CounterStrikeSharp.API.Modules.Utils;
namespace CounterStrikeSharp.API.Core.Translations;
public static class StringExtensions
{
public static string ReplaceColorTags(this string message)
{
var modifiedValue = message;
foreach (var field in typeof(ChatColors).GetFields())
{
string pattern = $"{{{field.Name}}}";
if (modifiedValue.Contains(pattern, StringComparison.OrdinalIgnoreCase))
{
modifiedValue = modifiedValue.Replace(pattern, field.GetValue(null)?.ToString() ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
}
return modifiedValue.Equals(message) ? message : $" {modifiedValue}";
}
}

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Reflection;
using Microsoft.Extensions.Localization;
namespace CounterStrikeSharp.API.Core.Translations;
internal class StringLocalizer : IStringLocalizer
{
private IStringLocalizer _localizer;
public StringLocalizer(IStringLocalizerFactory factory)
{
var type = typeof(StringLocalizer);
var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
_localizer = factory.Create(string.Empty, assemblyName.FullName);
}
public LocalizedString this[string name] => _localizer[name];
public LocalizedString this[string name, params object[] arguments] => _localizer[name, arguments];
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) => _localizer.GetAllStrings(includeParentCultures);
}

View File

@@ -0,0 +1,25 @@
using System.Globalization;
namespace CounterStrikeSharp.API.Core.Translations;
public sealed class WithTemporaryCulture : IDisposable
{
private readonly CultureInfo _originalCulture;
public WithTemporaryCulture(CultureInfo culture)
{
_originalCulture = CultureInfo.CurrentCulture;
SetCulture(culture);
}
private void SetCulture(CultureInfo cultureInfo)
{
CultureInfo.CurrentCulture = cultureInfo;
CultureInfo.CurrentUICulture = cultureInfo;
}
public void Dispose()
{
SetCulture(_originalCulture);
}
}

View File

@@ -25,6 +25,7 @@
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="7.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Scrutor" Version="4.2.2" />
<PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0" />

View File

@@ -0,0 +1,36 @@
/*
* This file is part of CounterStrikeSharp.
* CounterStrikeSharp is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CounterStrikeSharp is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
namespace CounterStrikeSharp.API
{
[Flags]
public enum VoiceFlags : Byte
{
Normal = 0,
Muted = (1 << 0),
All = (1 << 1),
ListenAll = (1 << 2),
Team = (1 << 3),
ListenTeam = (1 << 4),
}
public enum ListenOverride
{
Default = 0,
Mute,
Hear
}
}

View File

@@ -28,6 +28,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithEntityOutputHooks", "..
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CounterStrikeSharp.API.Tests", "CounterStrikeSharp.API.Tests\CounterStrikeSharp.API.Tests.csproj", "{BBA80E1B-109D-4ABD-9ADF-46EB0FEDFCD3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithTranslations", "..\examples\WithTranslations\WithTranslations.csproj", "{BB44E08E-CCA8-4E22-A132-11B2F69D1890}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithVoiceOverrides", "..\examples\WithVoiceOverrides\WithVoiceOverrides.csproj", "{6FA3107D-42AF-42A0-BF51-2230D13268B5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -86,6 +90,14 @@ Global
{BBA80E1B-109D-4ABD-9ADF-46EB0FEDFCD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BBA80E1B-109D-4ABD-9ADF-46EB0FEDFCD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BBA80E1B-109D-4ABD-9ADF-46EB0FEDFCD3}.Release|Any CPU.Build.0 = Release|Any CPU
{BB44E08E-CCA8-4E22-A132-11B2F69D1890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BB44E08E-CCA8-4E22-A132-11B2F69D1890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB44E08E-CCA8-4E22-A132-11B2F69D1890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB44E08E-CCA8-4E22-A132-11B2F69D1890}.Release|Any CPU.Build.0 = Release|Any CPU
{6FA3107D-42AF-42A0-BF51-2230D13268B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6FA3107D-42AF-42A0-BF51-2230D13268B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FA3107D-42AF-42A0-BF51-2230D13268B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6FA3107D-42AF-42A0-BF51-2230D13268B5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{57E64289-5D69-4AA1-BEF0-D0D96A55EE8F} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
@@ -97,5 +109,7 @@ Global
{3032F3FA-E20A-4581-9A08-2FB5FF1524F4} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{A641D8D7-35F1-48AB-AABA-EDFB6B7FC49B} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{31EABE0B-871F-497B-BF36-37FFC6FAD15F} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{BB44E08E-CCA8-4E22-A132-11B2F69D1890} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{6FA3107D-42AF-42A0-BF51-2230D13268B5} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
EndGlobalSection
EndGlobal

View File

@@ -16,6 +16,7 @@
using System;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
@@ -23,6 +24,7 @@ using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Entities;
@@ -31,6 +33,7 @@ using CounterStrikeSharp.API.Modules.Events;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
namespace TestPlugin
@@ -522,6 +525,13 @@ namespace TestPlugin
}
}
[ConsoleCommand("css_localetest", "Test Translations")]
public void OnCommandLocaleTest(CCSPlayerController? player, CommandInfo command)
{
Logger.LogInformation("Current Culture is {Culture}", CultureInfo.CurrentCulture);
command.ReplyToCommand(Localizer["testPlugin.maxPlayersAnnouncement", Server.MaxPlayers]);
}
[ConsoleCommand("css_sound", "Play a sound to client")]
public void OnCommandSound(CCSPlayerController? player, CommandInfo command)
{

View File

@@ -13,4 +13,10 @@
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="lang\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,3 @@
{
"testPlugin.maxPlayersAnnouncement": "The server currently has a max player count of {0} players (GB)"
}

View File

@@ -0,0 +1,3 @@
{
"testPlugin.maxPlayersAnnouncement": "The server currently has a max player count of {0} players",
}

View File

@@ -0,0 +1,3 @@
{
"testPlugin.maxPlayersAnnouncement": "Le serveur compte actuellement un nombre maximum de {0} joueurs."
}

View File

@@ -51,6 +51,7 @@ bool CCoreConfig::Init(char* conf_error, int conf_error_size)
SilentChatTrigger = m_json.value("SilentChatTrigger", SilentChatTrigger);
FollowCS2ServerGuidelines = m_json.value("FollowCS2ServerGuidelines", FollowCS2ServerGuidelines);
PluginHotReloadEnabled = m_json.value("PluginHotReloadEnabled", PluginHotReloadEnabled);
ServerLanguage = m_json.value("ServerLanguage", ServerLanguage);
} catch (const std::exception& ex) {
V_snprintf(conf_error, conf_error_size, "Failed to parse CoreConfig file: %s", ex.what());
return false;

View File

@@ -29,6 +29,7 @@ class CCoreConfig
std::vector<std::string> SilentChatTrigger = { std::string("/") };
bool FollowCS2ServerGuidelines = true;
bool PluginHotReloadEnabled = true;
std::string ServerLanguage = "en";
using json = nlohmann::json;
CCoreConfig(const std::string& path);

View File

@@ -1,3 +1,12 @@
// Copyright (C) 2023 neverlosecc
// See end of file for extended copyright information.
/**
* =============================================================================
* Source2Gen
* Copyright (C) 2023 neverlose (https://github.com/neverlosecc/source2gen)
* =============================================================================
**/
#pragma once
#include <vector>
@@ -392,3 +401,18 @@ class CSchemaSystem
return CALL_VIRTUAL(CSchemaSystemTypeScope*, 13, this, m_module_name, nullptr);
}
};
// source2gen - Source2 games SDK generator
// Copyright 2023 neverlosecc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

View File

@@ -20,6 +20,7 @@
#include "interfaces/cs2_interfaces.h"
#include "core/managers/entity_manager.h"
#include "core/managers/server_manager.h"
#include "core/managers/voice_manager.h"
#include <public/game/server/iplayerinfo.h>
#include <public/entity2/entitysystem.h>
@@ -77,6 +78,7 @@ ConCommandManager conCommandManager;
EntityManager entityManager;
ChatManager chatManager;
ServerManager serverManager;
VoiceManager voiceManager;
bool gameLoopInitialized = false;
GetLegacyGameEventListener_t* GetLegacyGameEventListener = nullptr;

View File

@@ -53,6 +53,7 @@ class HookManager;
class EntityManager;
class ChatManager;
class ServerManager;
class VoiceManager;
class CCoreConfig;
class CGameConfig;
@@ -98,6 +99,7 @@ extern TimerSystem timerSystem;
extern ChatCommands chatCommands;
extern ChatManager chatManager;
extern ServerManager serverManager;
extern VoiceManager voiceManager;
extern HookManager hookManager;
extern SourceHook::ISourceHook *source_hook;

View File

@@ -31,6 +31,7 @@
#include "core/managers/player_manager.h"
#include "core/managers/con_command_manager.h"
#include "core/managers/voice_manager.h"
#include <public/eiface.h>
#include <public/inetchannelinfo.h>
@@ -273,7 +274,7 @@ void PlayerManager::OnLevelEnd()
{
CSSHARP_CORE_TRACE("[PlayerManager][OnLevelEnd]");
for (int i = 0; i <= m_max_clients; i++) {
for (int i = 0; i <= MaxClients(); i++) {
if (m_players[i].IsConnected()) {
OnClientDisconnect(m_players[i].m_slot, ENetworkDisconnectionReason::NETWORK_DISCONNECT_INVALID, m_players[i].GetName(), 0,
m_players[i].GetIpAddress());
@@ -290,6 +291,8 @@ void PlayerManager::OnClientCommand(CPlayerSlot slot, const CCommand& args) cons
const char* cmd = args.Arg(0);
globals::voiceManager.OnClientCommand(slot, args);
auto result = globals::conCommandManager.ExecuteCommandCallbacks(
cmd, CCommandContext(CommandTarget_t::CT_NO_TARGET, slot), args, HookMode::Pre);
@@ -302,11 +305,11 @@ int PlayerManager::ListenClient() const { return m_listen_client; }
int PlayerManager::NumPlayers() const { return m_player_count; }
int PlayerManager::MaxClients() const { return m_max_clients; }
int PlayerManager::MaxClients() const { return globals::getGlobalVars()->maxClients; }
CPlayer* PlayerManager::GetPlayerBySlot(int client) const
{
if (client > m_max_clients || client < 0) {
if (client > MaxClients() || client < 0) {
return nullptr;
}
@@ -426,7 +429,6 @@ INetChannelInfo* CPlayer::GetNetInfo() const { return globals::engine->GetPlayer
PlayerManager::PlayerManager()
{
m_max_clients = 64;
m_players = new CPlayer[66];
m_player_count = 0;
m_user_id_lookup = new int[USHRT_MAX + 1];
@@ -441,7 +443,7 @@ void PlayerManager::RunAuthChecks()
m_last_auth_check_time = globals::timerSystem.GetTickedTime();
for (int i = 0; i <= m_max_clients; i++) {
for (int i = 0; i <= MaxClients(); i++) {
if (m_players[i].IsConnected()) {
if (m_players[i].IsAuthorized() || m_players[i].IsFakeClient())
continue;
@@ -532,6 +534,23 @@ float CPlayer::GetLatency() const
return GetNetInfo()->GetLatency(FLOW_INCOMING) + GetNetInfo()->GetLatency(FLOW_OUTGOING);
}
void CPlayer::SetListen(CPlayerSlot slot, ListenOverride listen)
{
m_listenMap[slot.Get()] = listen;
}
void CPlayer::SetVoiceFlags(VoiceFlag_t flags)
{
m_voiceFlag = flags;
}
VoiceFlag_t CPlayer::GetVoiceFlags() { return m_voiceFlag; }
ListenOverride CPlayer::GetListen(CPlayerSlot slot) const
{
return m_listenMap[slot.Get()];
}
void CPlayer::Connect()
{
if (m_is_in_game) {
@@ -551,6 +570,9 @@ void CPlayer::Disconnect()
m_user_id = -1;
m_is_authorized = false;
m_ip_address.clear();
m_selfMutes->ClearAll();
memset(m_listenMap, 0, sizeof m_listenMap);
m_voiceFlag = 0;
}
QAngle CPlayer::GetAbsAngles() const { return m_info->GetAbsAngles(); }

View File

@@ -45,6 +45,25 @@ namespace counterstrikesharp {
class ScriptCallback;
class CBaseEntityWrapper;
enum ListenOverride
{
Listen_Default = 0,
Listen_Mute,
Listen_Hear
};
enum VoiceFlagValue
{
Speak_Normal = 0,
Speak_Muted = 1 << 0,
Speak_All = 1 << 1,
Speak_ListenAll = 1 << 2,
Speak_Team = 1 << 3,
Speak_ListenTeam = 1 << 4,
};
typedef uint8_t VoiceFlag_t;
class CPlayer {
friend class PlayerManager;
@@ -92,6 +111,10 @@ public:
int GetUserId() const;
float GetTimeConnected() const;
float GetLatency() const;
void SetListen(CPlayerSlot slot, ListenOverride listen);
void SetVoiceFlags(VoiceFlag_t flags);
VoiceFlag_t GetVoiceFlags();
ListenOverride GetListen(CPlayerSlot slot) const;
public:
std::string m_name;
@@ -105,6 +128,9 @@ public:
CPlayerSlot m_slot = CPlayerSlot(-1);
const CSteamID* m_steamId;
std::string m_ip_address;
ListenOverride m_listenMap[66] = {};
VoiceFlag_t m_voiceFlag = 0;
CPlayerBitVec m_selfMutes[64] = {};
void SetName(const char *name);
INetChannelInfo *GetNetInfo() const;
};

View File

@@ -0,0 +1,129 @@
/*
* This file is part of CounterStrikeSharp.
* CounterStrikeSharp is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CounterStrikeSharp is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
#include "core/managers/voice_manager.h"
#include "core/managers/player_manager.h"
#include <public/eiface.h>
#include "scripting/callback_manager.h"
#include <schema.h>
#include <entity2/entitysystem.h>
SH_DECL_HOOK3(IVEngineServer2, SetClientListening, SH_NOATTRIB, 0, bool, CPlayerSlot, CPlayerSlot,
bool);
namespace counterstrikesharp {
VoiceManager::VoiceManager() {}
VoiceManager::~VoiceManager() {}
void VoiceManager::OnAllInitialized()
{
SH_ADD_HOOK(IVEngineServer2, SetClientListening, globals::engine,
SH_MEMBER(this, &VoiceManager::SetClientListening), false);
}
void VoiceManager::OnShutdown()
{
SH_REMOVE_HOOK(IVEngineServer2, SetClientListening, globals::engine,
SH_MEMBER(this, &VoiceManager::SetClientListening), false);
}
bool VoiceManager::SetClientListening(CPlayerSlot iReceiver, CPlayerSlot iSender, bool bListen)
{
auto pReceiver = globals::playerManager.GetPlayerBySlot(iReceiver.Get());
auto pSender = globals::playerManager.GetPlayerBySlot(iSender.Get());
if (pReceiver && pSender)
{
auto listenOverride = pReceiver->GetListen(iSender);
auto senderFlags = pSender->GetVoiceFlags();
auto receiverFlags = pReceiver->GetVoiceFlags();
if (pReceiver->m_selfMutes->Get(iSender.Get()))
{
RETURN_META_VALUE_NEWPARAMS(MRES_IGNORED, bListen, &IVEngineServer2::SetClientListening,
(iReceiver, iSender, false));
}
if (senderFlags & Speak_Muted)
{
RETURN_META_VALUE_NEWPARAMS(MRES_IGNORED, bListen, &IVEngineServer2::SetClientListening,
(iReceiver, iSender, false));
}
if (listenOverride == Listen_Mute)
{
RETURN_META_VALUE_NEWPARAMS(MRES_IGNORED, bListen, &IVEngineServer2::SetClientListening,
(iReceiver, iSender, false));
} else if (listenOverride == Listen_Hear) {
RETURN_META_VALUE_NEWPARAMS(MRES_IGNORED, bListen, &IVEngineServer2::SetClientListening,
(iReceiver, iSender, true));
}
if ((senderFlags & Speak_All) || (receiverFlags & Speak_ListenAll)) {
RETURN_META_VALUE_NEWPARAMS(MRES_IGNORED, bListen, &IVEngineServer2::SetClientListening,
(iReceiver, iSender, true));
}
if ((senderFlags & Speak_Team) || (receiverFlags & Speak_ListenTeam))
{
static auto classKey = hash_32_fnv1a_const("CBaseEntity");
static auto memberKey = hash_32_fnv1a_const("m_iTeamNum");
const static auto m_key = schema::GetOffset("CBaseEntity", classKey, "m_iTeamNum", memberKey);
auto receiverController = globals::entitySystem->GetBaseEntity(CEntityIndex(iReceiver.Get() + 1));
auto senderController = globals::entitySystem->GetBaseEntity(CEntityIndex(iSender.Get() + 1));
if (receiverController && senderController)
{
auto receiverTeam = *reinterpret_cast<std::add_pointer_t<unsigned int>>(
(uintptr_t)(receiverController) + m_key.offset);
auto senderTeam = *reinterpret_cast<std::add_pointer_t<unsigned int>>(
(uintptr_t)(senderController) + m_key.offset);
RETURN_META_VALUE_NEWPARAMS(MRES_IGNORED, bListen,
&IVEngineServer2::SetClientListening,
(iReceiver, iSender, receiverTeam == senderTeam));
}
}
}
RETURN_META_VALUE(MRES_IGNORED, bListen);
}
void VoiceManager::OnClientCommand(CPlayerSlot slot, const CCommand& args)
{
auto pPlayer = globals::playerManager.GetPlayerBySlot(slot.Get());
if (!pPlayer)
return;
if (args.ArgC() > 1 && stricmp(args.Arg(0), "vban") == 0)
{
// clients just refuse to send vban for indexes over 32 and all 4 fields are just the same number, so we only get the first one
//for (int i = 1; (i < args.ArgC()) && (i < 3); i++) {
unsigned int mask = 0;
sscanf(args.Arg(1), "%x", &mask);
pPlayer->m_selfMutes->SetDWord(0, mask);
//}
}
}
} // namespace counterstrikesharp

View File

@@ -0,0 +1,38 @@
/*
* This file is part of CounterStrikeSharp.
* CounterStrikeSharp is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CounterStrikeSharp is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
#pragma once
#include "core/globals.h"
#include "core/global_listener.h"
#include "scripting/script_engine.h"
namespace counterstrikesharp {
class ScriptCallback;
class VoiceManager : public GlobalClass
{
public:
VoiceManager();
~VoiceManager();
void OnAllInitialized() override;
void OnShutdown() override;
bool SetClientListening(CPlayerSlot iReceiver, CPlayerSlot iSender, bool bListen);
void OnClientCommand(CPlayerSlot slot, const CCommand& args);
private:
};
} // namespace counterstrikesharp

View File

@@ -0,0 +1,129 @@
/*
* This file is part of CounterStrikeSharp.
* CounterStrikeSharp is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CounterStrikeSharp is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
*/
#include "scripting/autonative.h"
#include "scripting/script_engine.h"
#include "core/managers/player_manager.h"
#include <public/entity2/entitysystem.h>
namespace counterstrikesharp {
void SetClientListening(ScriptContext& scriptContext)
{
auto receiver = scriptContext.GetArgument<CBaseEntity*>(0);
auto sender = scriptContext.GetArgument<CBaseEntity*>(1);
auto listen = scriptContext.GetArgument<ListenOverride>(2);
if (!receiver) {
scriptContext.ThrowNativeError("Receiver is a null pointer");
return;
}
if (!sender) {
scriptContext.ThrowNativeError("Sender is a null pointer");
return;
}
auto iSenderSlot = sender->GetEntityIndex().Get() - 1;
if (iSenderSlot < 0 || iSenderSlot >= globals::getGlobalVars()->maxClients)
scriptContext.ThrowNativeError("Invalid sender");
auto pPlayer = globals::playerManager.GetPlayerBySlot(receiver->GetEntityIndex().Get() - 1);
if (pPlayer == nullptr) {
scriptContext.ThrowNativeError("Invalid receiver");
return;
}
pPlayer->SetListen(iSenderSlot, listen);
}
ListenOverride GetClientListening(ScriptContext& scriptContext)
{
auto receiver = scriptContext.GetArgument<CBaseEntity*>(0);
auto sender = scriptContext.GetArgument<CBaseEntity*>(1);
if (!receiver) {
scriptContext.ThrowNativeError("Receiver is a null pointer");
return Listen_Default;
}
if (!sender) {
scriptContext.ThrowNativeError("Sender is a null pointer");
return Listen_Default;
}
auto iSenderSlot = sender->GetEntityIndex().Get() - 1;
if (iSenderSlot < 0 || iSenderSlot >= globals::getGlobalVars()->maxClients)
scriptContext.ThrowNativeError("Invalid sender");
auto pPlayer = globals::playerManager.GetPlayerBySlot(receiver->GetEntityIndex().Get() - 1);
if (pPlayer == nullptr) {
scriptContext.ThrowNativeError("Invalid receiver");
return Listen_Default;
}
return pPlayer->GetListen(iSenderSlot);
}
void SetClientVoiceFlags(ScriptContext& scriptContext)
{
auto client = scriptContext.GetArgument<CBaseEntity*>(0);
auto flags = scriptContext.GetArgument<VoiceFlag_t>(1);
if (!client) {
scriptContext.ThrowNativeError("Receiver is a null pointer");
return;
}
auto pPlayer = globals::playerManager.GetPlayerBySlot(client->GetEntityIndex().Get() - 1);
if (pPlayer == nullptr) {
scriptContext.ThrowNativeError("Invalid receiver");
return;
}
pPlayer->SetVoiceFlags(flags);
}
VoiceFlag_t GetClientVoiceFlags(ScriptContext& scriptContext)
{
auto client = scriptContext.GetArgument<CBaseEntity*>(0);
if (!client) {
scriptContext.ThrowNativeError("Receiver is a null pointer");
return VoiceFlag_t{};
}
auto pPlayer = globals::playerManager.GetPlayerBySlot(client->GetEntityIndex().Get() - 1);
if (pPlayer == nullptr) {
scriptContext.ThrowNativeError("Invalid receiver");
}
return pPlayer->GetVoiceFlags();
}
REGISTER_NATIVES(voice, {
ScriptEngine::RegisterNativeHandler("SET_CLIENT_LISTENING", SetClientListening);
ScriptEngine::RegisterNativeHandler("GET_CLIENT_LISTENING", GetClientListening);
ScriptEngine::RegisterNativeHandler("SET_CLIENT_VOICE_FLAGS", SetClientVoiceFlags);
ScriptEngine::RegisterNativeHandler("GET_CLIENT_VOICE_FLAGS", GetClientVoiceFlags);
})
} // namespace counterstrikesharp

View File

@@ -0,0 +1,4 @@
SET_CLIENT_LISTENING: receiver:pointer, sender:pointer, listen:uint -> void
GET_CLIENT_LISTENING: receiver:pointer, sender:pointer -> ListenOverride
SET_CLIENT_VOICE_FLAGS: client:pointer, flags:uint -> void
GET_CLIENT_VOICE_FLAGS: client:pointer -> uint

View File

@@ -63,6 +63,8 @@ public class Mapping
return "[CastFrom(typeof(ulong))]SteamID";
case "HookMode":
return "HookMode";
case "ListenOverride":
return "ListenOverride";
case "DataType_t":
return "DataType";
case "any":