mirror of
https://github.com/roflmuffin/CounterStrikeSharp.git
synced 2025-12-06 08:03:12 -08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44e3f2240c | ||
|
|
8af219e7a8 | ||
|
|
bff04e7795 | ||
|
|
d495ac6230 | ||
|
|
f78abf0c81 |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
5
docfx/examples/WithVoiceOverrides.md
Normal file
5
docfx/examples/WithVoiceOverrides.md
Normal 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)]
|
||||
@@ -15,5 +15,7 @@ items:
|
||||
href: WithDatabase.md
|
||||
- name: Translations
|
||||
href: WithTranslations.md
|
||||
- name: Voice Overrides
|
||||
href: WithVoiceOverrides.md
|
||||
- name: Warcraft Plugin
|
||||
href: WarcraftPlugin.md
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
2
examples/WithVoiceOverrides/README.md
Normal file
2
examples/WithVoiceOverrides/README.md
Normal 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.
|
||||
12
examples/WithVoiceOverrides/WithVoiceOverrides.csproj
Normal file
12
examples/WithVoiceOverrides/WithVoiceOverrides.csproj
Normal 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>
|
||||
79
examples/WithVoiceOverrides/WithVoiceOverridesPlugin.cs
Normal file
79
examples/WithVoiceOverrides/WithVoiceOverridesPlugin.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Submodule libraries/hl2sdk-cs2 updated: 1d394d3365...9363452257
@@ -34,6 +34,11 @@ public class TranslationTests
|
||||
{
|
||||
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")))
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,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;
|
||||
|
||||
@@ -150,9 +150,18 @@ namespace CounterStrikeSharp.API.Core
|
||||
.FirstOrDefault(x => x.Name == ServerLanguage);
|
||||
if (serverCulture == null)
|
||||
{
|
||||
_logger.LogWarning("Server Language \"{ServerLanguage}\" is not supported, falling back to \"en\"", ServerLanguage);
|
||||
serverCulture = new CultureInfo("en");
|
||||
_coreConfig.ServerLanguage = "en";
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,14 @@ namespace CounterStrikeSharp.API.Core.Translations
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -53,6 +61,21 @@ namespace CounterStrikeSharp.API.Core.Translations
|
||||
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)
|
||||
{
|
||||
@@ -93,20 +116,25 @@ namespace CounterStrikeSharp.API.Core.Translations
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
private void TryLoadResourceSet(CultureInfo culture)
|
||||
|
||||
private void TryLoadResourceSet(string cultureName)
|
||||
{
|
||||
if (!_resourcesCache.ContainsKey(culture.Name))
|
||||
if (!_resourcesCache.ContainsKey(cultureName))
|
||||
{
|
||||
var file = Path.Combine(ResourcesPath, $"{culture.Name}.json");
|
||||
var file = Path.Combine(ResourcesPath, $"{cultureName}.json");
|
||||
|
||||
var resources = LoadJsonResources(file);
|
||||
|
||||
_resourcesCache.TryAdd(culture.Name,
|
||||
_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>();
|
||||
|
||||
@@ -64,6 +64,12 @@ public class JsonStringLocalizer : IStringLocalizer
|
||||
|
||||
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))
|
||||
{
|
||||
|
||||
36
managed/CounterStrikeSharp.API/VoiceFlags.cs
Normal file
36
managed/CounterStrikeSharp.API/VoiceFlags.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CounterStrikeSharp.API.Test
|
||||
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
|
||||
@@ -92,6 +94,10 @@ Global
|
||||
{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}
|
||||
@@ -104,5 +110,6 @@ Global
|
||||
{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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
129
src/core/managers/voice_manager.cpp
Normal file
129
src/core/managers/voice_manager.cpp
Normal 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
|
||||
38
src/core/managers/voice_manager.h
Normal file
38
src/core/managers/voice_manager.h
Normal 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
|
||||
129
src/scripting/natives/natives_voice.cpp
Normal file
129
src/scripting/natives/natives_voice.cpp
Normal 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
|
||||
4
src/scripting/natives/natives_voice.yaml
Normal file
4
src/scripting/natives/natives_voice.yaml
Normal 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
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user