Skip to content

Example: Infected

Julgers edited this page Nov 27, 2024 · 2 revisions

In this example, we create a game where you can become 'infected'. Any non-infected player that gets killed by an infected player becomes 'infected'.

This one is written with the goal of showcasing what kind of things can be done with the community API, more so than being a fully complete game mode that you can drop in. I encourage you to play around with it yourself and fine-tune it before using it.

To get started, make a fresh .NET 6.0 project, and install the community server API inside of it:

dotnet add package CommunityServerAPI

Then we shall write our main API logic.

program.cs:

using System.Net;
using BattleBitAPI;
using BattleBitAPI.Common;
using BattleBitAPI.Server;

namespace Infected;

internal static class Program
{
    private static void Main(string[] args)
    {
        var listener = new ServerListener<MyPlayer, MyGameServer>();
        
        // Add callbacks to the listener. This listener does not really contain any in-game event callbacks
        // All of the callbacks are specific to the API events, such as game servers trying to connect etc.
        // We can override functions for in-game events in the MyPlayer and MyGameServer classes.
        listener.OnCreatingGameServerInstance = OnCreatingGameServerInstance;
        listener.OnCreatingPlayerInstance = OnCreatingPlayerInstance;
        listener.OnValidateGameServerToken = OnValidateGameServerToken;
        listener.OnGameServerConnected = OnGameServerConnected;
        listener.OnGameServerDisconnected = OnGameServerDisconnected;
        listener.Start(29294);

        // The listener is running, now we just want this thread to sleep so that the program does not exit.
        // Alternatively, you could make the listener a hosted service, which allows for more flexibility including
        // dependency injection. See https://github.com/Julgers/Database-connection-example/blob/main/Program.cs for an
        // example on how to do that.
        Thread.Sleep(Timeout.Infinite);
    }

    private static async Task OnGameServerDisconnected(GameServer<MyPlayer> arg)
    {
        await Console.Out.WriteLineAsync($"Gameserver {arg.GameIP}:{arg.GamePort} disconnected.");
    }

    private static async Task OnGameServerConnected(GameServer<MyPlayer> arg)
    {
        await Console.Out.WriteLineAsync($"Gameserver {arg.GameIP}:{arg.GamePort} connected.");
    }

    // With this function, you can require any game server that wants to connect to your API to send an API token.
    // For this, you would need to add "-apiToken=Your_super_secret_token" to the startup parameters of the game server(s).
    private static async Task<bool> OnValidateGameServerToken(IPAddress ip, ushort port, string token)
    {
        await Console.Out.WriteLineAsync($"{ip}:{port} sent {token}");
        return token == "Your_super_secret_token";
    }

    // Use these functions if you want to pass things to the constructors of your MyPlayer & MyGameServer.
    // This could be anything, including something like a service provider. Or just simple stuff like the URL of your
    // Discord webhook... All up to you ¯\_(ツ)_/¯
    private static MyPlayer OnCreatingPlayerInstance(ulong steamId)
    {
        return new MyPlayer();
    }
    private static MyGameServer OnCreatingGameServerInstance(IPAddress ip, ushort port)
    {
        return new MyGameServer();
    }
}

// We can now start with MyPlayer and MyGameserver. We can give them private properties/fields and then
// add overrides for the callbacks for in-game events, and also requests from the gameserver that we can deny/accept/change.
internal class MyPlayer : Player<MyPlayer>
{
    public bool IsInfected;
    
    // We reset this when a new game starts etc.
    public override async Task OnSessionChanged(long oldSessionId, long newSessionId)
    {
        IsInfected = false;
    }
}

internal class MyGameServer : GameServer<MyPlayer>
{
    public override async Task OnConnected()
    {
        ForceStartGame();
        ServerSettings.PlayerCollision = true;
    }
    // ----------------- Public override -----------------
    public override async Task OnRoundStarted()
    {
        // Right now we should have no one who is infected.
        InfectRando();

        // We need to make sure everyone's in the right team when the game starts.
        foreach (var player in AllPlayers)
        {
            // Team B for infected, A for non-infected
            player.ChangeTeam(player.IsInfected ? Team.TeamB : Team.TeamA);
        }
    }

    public override async Task OnAPlayerDownedAnotherPlayer(OnPlayerKillArguments<MyPlayer> args)
    {
        // Technically this should never occur, but just to be safe.
        if (!args.Killer.IsInfected)
            return;
        
        InfectPlayer(args.Victim);
    }

    public override async Task<bool> OnPlayerRequestingToChangeTeam(MyPlayer player, Team requestedTeam)
    {
        // No, we determine what team they're in, not them.
        return false;
    }

    public override async Task<OnPlayerSpawnArguments?> OnPlayerSpawning(MyPlayer player, OnPlayerSpawnArguments request)
    {
        // This function is a REQUEST, meaning that you can DENY spawn requests of people if they have/do something you don't like.
        // All you would have to do is return null and their spawn request is denied. Maybe do player.message() do let them know why
        // you denied their spawn request, but the specific implementations are all up to you.
        
        // We do player.PutIntoTeam() here to double check that the player didn't just swap teams themselves.
        if (player.IsInfected)
        {
            // No primary
            request.Loadout.PrimaryWeapon = default;
            
            // No secondary
            request.Loadout.SecondaryWeapon = default;
            
            // No light gadget
            request.Loadout.LightGadget = null;
            
            // Sledgehammer
            request.Loadout.HeavyGadget = Gadgets.SledgeHammer;
            
            // No nades
            request.Loadout.Throwable = null;
            
            // No spawn protection
            request.SpawnProtection = 0f;
        }
        else // Whatever you want to do when a non-infected player is spawning.
        {
            // force primary weapon to M4A1
            request.Loadout.PrimaryWeapon.Tool = Weapons.M4A1;

            // Give 10 extra magazines on primary
            request.Loadout.PrimaryExtraMagazines += 10;

            // Give 5 extra throwables
            request.Loadout.ThrowableExtra += 5;

        }

        return request;
    }

    public override async Task OnPlayerSpawned(MyPlayer player)
    {
        if(player.IsInfected) // Give funny bonuses to infected players.
        {
            player.Modifications.RunningSpeedMultiplier = 2f;
            player.Modifications.JumpHeightMultiplier = 2f;
            player.Modifications.FallDamageMultiplier = 0f;
            player.Modifications.ReceiveDamageMultiplier = 0.5f;
            player.Modifications.GiveDamageMultiplier = 4f;
        }
    }

    // ------------- Private -----------
    private void InfectRando()
    {
        // The one who gets infected is someone at a random index of AllPlayers.
        // For random index generation, keep in mind that arrays start at 0 :^)
        // We generate an index between 0 and the size of `AllPlayers` - 1.
        InfectPlayer(AllPlayers.ElementAt(Random.Shared.Next(0, AllPlayers.Count() - 1)));
    }
    
    private void InfectPlayer(MyPlayer player)
    {
        player.IsInfected = true;
        player.Message("You have been infected!");
        
        player.ChangeTeam(Team.TeamB);

        AnnounceShort($"{player.Name} has been infected!");
        
        if (!AllPlayers.All(p => p.IsInfected)) return;
        
        AnnounceShort("Everyone has been infected!");
        ForceEndGame();
    }
    
}
Clone this wiki locally