Skip to content

Commit

Permalink
Feat/raffle (#29)
Browse files Browse the repository at this point in the history
* Add raffle system

* Fix localization parity
  • Loading branch information
MSWS authored Oct 22, 2024
1 parent 7f451e1 commit cecd484
Show file tree
Hide file tree
Showing 14 changed files with 319 additions and 2 deletions.
7 changes: 7 additions & 0 deletions Gangs.sln
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StatsTracker", "src\StatsTr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EcoRewards", "src\EcoRewards\EcoRewards.csproj", "{253C7948-3411-4860-BDDE-B1CA23FCE4DC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Raffle", "Raffle\Raffle.csproj", "{05B36B8C-F430-411B-9B8D-61EB14D5E5FC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -78,6 +80,10 @@ Global
{253C7948-3411-4860-BDDE-B1CA23FCE4DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{253C7948-3411-4860-BDDE-B1CA23FCE4DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{253C7948-3411-4860-BDDE-B1CA23FCE4DC}.Release|Any CPU.Build.0 = Release|Any CPU
{05B36B8C-F430-411B-9B8D-61EB14D5E5FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{05B36B8C-F430-411B-9B8D-61EB14D5E5FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05B36B8C-F430-411B-9B8D-61EB14D5E5FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05B36B8C-F430-411B-9B8D-61EB14D5E5FC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{74B15261-4B12-4EF6-859A-E46B315E7DD3} = {3AB7703F-880F-4A41-96EE-B891FA888C65}
Expand All @@ -90,5 +96,6 @@ Global
{1899055E-62B8-4907-85A2-DFE22531729E} = {32D392D5-C809-45A1-A5FB-58C941D86057}
{B850CFA3-AFE8-4012-8BC2-9A4BC12B9748} = {AC07CD29-5C9D-4AD1-99C7-01DABAB8D0EC}
{253C7948-3411-4860-BDDE-B1CA23FCE4DC} = {AC07CD29-5C9D-4AD1-99C7-01DABAB8D0EC}
{05B36B8C-F430-411B-9B8D-61EB14D5E5FC} = {AC07CD29-5C9D-4AD1-99C7-01DABAB8D0EC}
EndGlobalSection
EndGlobal
10 changes: 10 additions & 0 deletions Raffle/IRaffleManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Raffle;

public interface IRaffleManager {
Raffle? Raffle { get; }

bool StartRaffle(int buyIn);
bool AreEntriesOpen();
void SetEntriesOpen(float seconds);
void DrawWinner();
}
27 changes: 27 additions & 0 deletions Raffle/Raffle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Raffle;

public class Raffle(int buyIn) {
private readonly HashSet<ulong> players = [];

public int BuyIn => buyIn;

public int Value => players.Count * buyIn;

public int TotalPlayers => players.Count;

public void AddPlayer(ulong player) { players.Add(player); }

/// <summary>
/// Get the winner of the raffle
/// </summary>
/// <returns>The winner, or null if there are no players</returns>
public ulong? GetWinner() {
if (players.Count == 0) return null;
var random = new Random();
var winnerIndex = random.Next(players.Count);
// remove the winner from the list of players
var winner = players.ElementAt(winnerIndex);
players.Remove(winner);
return winner;
}
}
13 changes: 13 additions & 0 deletions Raffle/Raffle.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\src\GangsAPI\GangsAPI.csproj"/>
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions Raffle/RaffleCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using GangsAPI.Extensions;
using Microsoft.Extensions.DependencyInjection;

namespace Raffle;

public static class RaffleCollection {
public static void RegisterRaffle(this IServiceCollection provider) {
provider.AddPluginBehavior<IRaffleManager, RaffleManager>();
provider.AddPluginBehavior<RaffleCommand>();
provider.AddPluginBehavior<StartRaffleCommand>();
}
}
37 changes: 37 additions & 0 deletions Raffle/RaffleCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using GangsAPI.Data;
using GangsAPI.Data.Command;
using GangsAPI.Services;
using GangsAPI.Services.Commands;
using Microsoft.Extensions.DependencyInjection;

namespace Raffle;

public class RaffleCommand(IServiceProvider provider) : ICommand {
private readonly IEcoManager eco = provider.GetRequiredService<IEcoManager>();

private readonly IRaffleManager raffle =
provider.GetRequiredService<IRaffleManager>();

public string Name => "css_raffle";

public async Task<CommandResult> Execute(PlayerWrapper? executor,
CommandInfoWrapper info) {
if (executor == null) return CommandResult.PLAYER_ONLY;
if (info.ArgCount != 1) return CommandResult.PRINT_USAGE;

if (raffle.Raffle == null)
// no raffle is currently running
return CommandResult.SUCCESS;

if (!raffle.AreEntriesOpen())
// entries are closed
return CommandResult.SUCCESS;

if (await eco.TryPurchase(executor, raffle.Raffle.BuyIn, true, "Raffle Ticket",
true) < 0)
return CommandResult.SUCCESS;

raffle.Raffle.AddPlayer(executor.Steam);
return CommandResult.SUCCESS;
}
}
105 changes: 105 additions & 0 deletions Raffle/RaffleManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Cvars;
using GangsAPI;
using Microsoft.Extensions.Localization;
using Timer = CounterStrikeSharp.API.Modules.Timers.Timer;

namespace Raffle;

public class RaffleManager(IStringLocalizer locale)
: IPluginBehavior, IRaffleManager {
public static FakeConVar<float> CV_RAFFLE_CHANCE =
new("cs2_gangs_raffle_chance", "The chance of a raffle starting per round",
0.1f);

public static FakeConVar<int> CV_RAFFLE_COOLDOWN =
new("cs2_gangs_raffle_cooldown", "Minimum number of rounds between raffles",
2);

public static FakeConVar<int> CV_RAFFLE_MINIMUM =
new("cs2_gangs_raffle_min", "Minimum amount per player", 2);

public static FakeConVar<int> CV_RAFFLE_MAXIMUM =
new("cs2_gangs_raffle_max", "Maximum amount per player", 2);

public static FakeConVar<float> CV_RAFFLE_DURATION =
new("cs2_gangs_raffle_duration", "Time to give playeres to enter raffle",
20);

private static readonly Random rng = new();
private int cooldownRounds;

private Timer? entryTimer;
private BasePlugin? plugin;

public void Start(BasePlugin? plugin, bool hotReload) {
if (plugin == null) return;
this.plugin = plugin;
}

public Raffle? Raffle { get; private set; }

public bool StartRaffle(int buyIn) {
if (Raffle != null || plugin == null) return false;
Raffle = new Raffle(buyIn);

SetEntriesOpen(CV_RAFFLE_DURATION.Value);
return true;
}

public bool AreEntriesOpen() { return entryTimer != null; }

public void SetEntriesOpen(float seconds) {
entryTimer?.Kill();
if (plugin == null || Raffle == null) return;
Server.PrintToChatAll(locale.Get(MSG.RAFFLE_BEGIN, Raffle?.BuyIn ?? 0));

entryTimer = plugin.AddTimer(seconds, () => {
entryTimer = null;
if (Raffle == null) return;

Server.PrintToChatAll(locale.Get(MSG.RAFFLE_PRE_ANNOUNCE, Raffle.Value,
Raffle.TotalPlayers));

plugin.AddTimer(5, DrawWinner);
});
}

public void DrawWinner() {
if (Raffle == null || plugin == null) return;
ulong? winner;
do { winner = Raffle.GetWinner(); } while (winner != null
&& Raffle.TotalPlayers > 0);

if (winner == null) {
Server.PrintToChatAll(locale.Get(MSG.GENERIC_ERROR_INFO,
"Could not find a winner"));
Raffle = null;
return;
}

var name = Utilities.GetPlayerFromSteamId(winner.Value)?.PlayerName
?? winner.ToString() ?? "";

Server.PrintToChatAll(locale.Get(MSG.RAFFLE_WINNER, name,
(1.0f / (Raffle.TotalPlayers + 1)).ToString("P1")));
Raffle = null;
}

[GameEventHandler]
public HookResult OnRoundStart(EventRoundStart ev, GameEventInfo info) {
if (RoundUtil.IsWarmup()) return HookResult.Continue;
if (cooldownRounds > 0) {
cooldownRounds--;
return HookResult.Continue;
}

if (rng.NextDouble() > CV_RAFFLE_CHANCE.Value) return HookResult.Continue;
var amo = rng.Next(CV_RAFFLE_MINIMUM.Value, CV_RAFFLE_MAXIMUM.Value);
StartRaffle(amo);
cooldownRounds = CV_RAFFLE_COOLDOWN.Value;
return HookResult.Continue;
}
}
42 changes: 42 additions & 0 deletions Raffle/RoundUtil.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using CounterStrikeSharp.API;

namespace Raffle;

public static class RoundUtil {
public static int GetTimeElapsed() {
var gamerules = ServerExtensions.GetGameRules();
if (gamerules == null) return 0;
var freezeTime = gamerules.FreezeTime;
return (int)(Server.CurrentTime - gamerules.RoundStartTime - freezeTime);
}

public static int GetTimeRemaining() {
var gamerules = ServerExtensions.GetGameRules();
if (gamerules == null) return 0;
return gamerules.RoundTime - GetTimeElapsed();
}

public static void SetTimeRemaining(int seconds) {
var gamerules = ServerExtensions.GetGameRules();
if (gamerules == null) return;
gamerules.RoundTime = GetTimeElapsed() + seconds;
var proxy = ServerExtensions.GetGameRulesProxy();
if (proxy == null) return;
Utilities.SetStateChanged(proxy, "CCSGameRulesProxy", "m_pGameRules");
}

public static void AddTimeRemaining(int time) {
var gamerules = ServerExtensions.GetGameRules();
if (gamerules == null) return;
gamerules.RoundTime += time;

var proxy = ServerExtensions.GetGameRulesProxy();
if (proxy == null) return;
Utilities.SetStateChanged(proxy, "CCSGameRulesProxy", "m_pGameRules");
}

public static bool IsWarmup() {
var rules = ServerExtensions.GetGameRules();
return rules == null || rules.WarmupPeriod;
}
}
25 changes: 25 additions & 0 deletions Raffle/ServerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;

namespace Raffle;

public static class ServerExtensions {
/// <summary>
/// Get the current CCSGameRules for the server
/// </summary>
/// <returns></returns>
public static CCSGameRules? GetGameRules() {
// From killstr3ak
return Utilities
.FindAllEntitiesByDesignerName<CCSGameRulesProxy>("cs_gamerules")
.First()
.GameRules;
}

public static CCSGameRulesProxy? GetGameRulesProxy() {
// From killstr3ak
return Utilities
.FindAllEntitiesByDesignerName<CCSGameRulesProxy>("cs_gamerules")
.FirstOrDefault();
}
}
26 changes: 26 additions & 0 deletions Raffle/StartRaffleCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using GangsAPI.Data;
using GangsAPI.Data.Command;
using GangsAPI.Services.Commands;
using Microsoft.Extensions.DependencyInjection;

namespace Raffle;

public class StartRaffleCommand(IServiceProvider provider) : ICommand {
private readonly IRaffleManager raffle =
provider.GetRequiredService<IRaffleManager>();

public string Name => "css_startraffle";
public string[] RequiredFlags => ["@css/root"];
public string[] Usage => ["", "<amount>"];

public Task<CommandResult> Execute(PlayerWrapper? executor,
CommandInfoWrapper info) {
var amo = 100;
if (info.ArgCount == 2)
if (!int.TryParse(info.Args[1], out amo))
return Task.FromResult(CommandResult.PRINT_USAGE);

raffle.StartRaffle(amo);
return Task.FromResult(CommandResult.SUCCESS);
}
}
5 changes: 4 additions & 1 deletion lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,8 @@
"rank.cannot.owner": "%prefix%You cannot {0} as the owner, use %color.command%/gang transfer <player>%color.default% to transfer ownership.",
"command.invite.doorpolicy": "%prefix%Your door policy must be set to invite-only to send invites.",
"menu.format.invitation": "%color.special%{0}%color.default% invited %color.target%{1}%color.default% on %color.emph%{2}%color.default%.",
"menu.format.request": "%color.target%{0}%color.default% requested to join on %color.emph%{1}%color.default%."
"menu.format.request": "%color.target%{0}%color.default% requested to join on %color.emph%{1}%color.default%.",
"raffle.begin": "%prefix%A raffle for %color.currency%{0} %currency%%s%%color.default% has begun. Type %color.command%/raffle %color.default% to enter.",
"raffle.preannounce": "%prefix%Raffle closed! Collected %color.currency%{0} %currency%%s% across %color.number%{1}%color.default% entrant%s%.",
"raffle.winner": "%prefix%%color.target%{0}%color.default% won the raffle with a %color.number%{1}%color.default% chance!"
}
2 changes: 2 additions & 0 deletions src/CS2/Gangs/GangServiceCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Localization;
using Mock;
using Raffle;
using SQLImpl;
using Stats.Perk;
using StatsTracker;
Expand Down Expand Up @@ -41,6 +42,7 @@ public void ConfigureServices(IServiceCollection serviceCollection) {
serviceCollection.RegisterStatsTracker();
serviceCollection.RegisterPerks();
serviceCollection.RegisterRewards();
serviceCollection.RegisterRaffle();

serviceCollection.AddPluginBehavior<PlayerJoinCreationListener>();
serviceCollection
Expand Down
1 change: 1 addition & 0 deletions src/CS2/Gangs/Gangs.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\Raffle\Raffle.csproj" />
<ProjectReference Include="..\..\EcoRewards\EcoRewards.csproj"/>
<ProjectReference Include="..\..\StatsTracker\StatsTracker.csproj"/>
<ProjectReference Include="..\Commands\Commands.csproj"/>
Expand Down
9 changes: 8 additions & 1 deletion src/GangsAPI/MSG.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,11 @@ public enum MSG {
COMMAND_GANG_RESTRICTED,
COMMAND_INVITE_DOORPOLICY,
MENU_FORMAT_INVITATION,
MENU_FORMAT_REQUEST, }
MENU_FORMAT_REQUEST,
RAFFLE_BEGIN,
RAFFLE_PRE_ANNOUNCE,
RAFFLE_WINNER
}

public static class LocaleExtensions {
public static string Key(this MSG msg) {
Expand Down Expand Up @@ -230,6 +234,9 @@ public static string Key(this MSG msg) {
MSG.COMMAND_INVITE_DOORPOLICY => "command.invite.doorpolicy",
MSG.MENU_FORMAT_INVITATION => "menu.format.invitation",
MSG.MENU_FORMAT_REQUEST => "menu.format.request",
MSG.RAFFLE_BEGIN => "raffle.begin",
MSG.RAFFLE_PRE_ANNOUNCE => "raffle.preannounce",
MSG.RAFFLE_WINNER => "raffle.winner",
_ => throw new ArgumentOutOfRangeException(nameof(msg), msg, null)
};
}
Expand Down

0 comments on commit cecd484

Please sign in to comment.