[Script Module] Custom Spawn Selection

Started by DizzasTeR, Apr 05, 2020, 10:12 AM

Previous topic - Next topic

DizzasTeR

A basic implementation of vanilla based spawn selection.

NOTE: THIS APPLIES TO ALL SERVERS. WHETHER THEY ARE USING OFFICIAL SERVER PLUGIN, SQMOD OR ANYTHING ELSE AS FAR AS I KNOW
Fixed in later server updates.

— Why?
People on our unofficial VCMP Discord figured out (specifically @aXXo) that by just staying at the skin/spawn selection screen for a few minutes, will make VCMP server start leaking memory, which will start lagging and eventually just crash the server.

— How to solve it?
The solution is to not let players stay at skin selection for too long. Some servers perform various things at Request class and Request spawn events, therefore its not a solution for them to just force spawn player all the time, one alternative is to make your own spawn selection.

You can place this code inside a spawnselector.nut file and just load it from sqmod.ini as Compile

[noae][noae][noae][noae][noae][noae]__SPAWN_SELECTOR_STATICS__ <- {
    SPAWN_SELECTION_ID = -1,
    CLASS_ID = 0,
    PLAYER_LAST_USED_CLASS = {}
};

class SpawnSelector {
    static PlayerRequestClass = SqCreateSignal("PlayerRequestClass");
    static PlayerRequestSpawn = SqCreateSignal("PlayerRequestSpawn");
    static PlayerSpawn        = SqCreateSignal("PlayerSpawn");

    static SPAWN_SELECTION_KEYS = {
        ARROW_LEFT  = SqKeybind.Create(true, 0x25, 0, 0),
        ARROW_RIGHT = SqKeybind.Create(true, 0x27, 0, 0),

        SELECTION_CTRL  = SqKeybind.Create(true, 0x11, 0, 0),
        SELECTION_NUM0  = SqKeybind.Create(true, 0x60, 0, 0),
        SELECTION_LMB   = SqKeybind.Create(true, 0x01, 0, 0)
    };

    ID      = null;
    Camera  = null;
    Classes = null;
    Players = null;

    constructor(cameraData = null) {
        ::__SPAWN_SELECTOR_STATICS__.SPAWN_SELECTION_ID++;
        ID = ::__SPAWN_SELECTOR_STATICS__.SPAWN_SELECTION_ID;

        Camera = {
            position        = ::Vector3(694.918, -652.151, 12.1429),
            lookAt          = ::Vector3(687.879, -652.088, 12.08),
            playerPosition  = ::Vector3(687.879, -652.088, 12.08)
        }

        Classes = [];
        Players = {};

        if(cameraData)
            SetCameraData(cameraData.position, cameraData.lookAt, cameraData.playerPosition);
    }

    /*** Camera ***/

    function SetCameraData(position, lookAt, playerPosition) {
        SetCameraPosition(position);
        SetCameraLookAt(lookAt);
        SetCameraPlayerPosition(playerPosition);
    }

    function SetCameraPosition(position) {
        Camera.position = position;
    }

    function SetCameraLookAt(lookAt) {
        Camera.lookAt = lookAt;
    }

    function SetCameraPlayerPosition(position) {
        Camera.playerPosition = position;
    }

    /***
        Methods
    ***/

    /*** Player ***/

    function AddPlayer(player, forceSetClass = 0) {
        if(Classes.len() == 0)
            throw("Class selector has no classes");

        if(!IsActiveForPlayer(player)) {
            Players[player.ID] <- SpawnSelectorPlayer(player, Classes[forceSetClass]);
            player.SetOption(::getconsttable().SqPlayerOption.Controllable, false);
           
            player.World = player.UniqueWorld;
            player.Pos = Camera.playerPosition;
            player.CameraPosition(Camera.position.x, Camera.position.y, Camera.position.z, Camera.lookAt.x, Camera.lookAt.y, Camera.lookAt.z);
        }
    }

    function RemovePlayer(player) {
        if(IsActiveForPlayer(player)) {
            Players.rawget(player.ID).destroy();
            Players.rawdelete(player.ID);
            player.SetOption(::getconsttable().SqPlayerOption.Controllable, true);
            player.World = 1;
        }
    }

    function IsActiveForPlayer(player) {
        return Players.rawin(player.ID);
    }

    function RequestSpawn(player) {
        if(IsActiveForPlayer(player)) {
            if(!PlayerRequestSpawn.Consume(player, Players.rawget(player.ID)))
                return false;
            SpawnPlayer(player);
        }
    }

    function SpawnPlayer(player) {
        local classID = Players.rawget(player.ID).ClassID;
        Classes[classID].Apply(player);
        RemovePlayer(player);
        PlayerSpawn.Consume(player);
        ::__SPAWN_SELECTOR_STATICS__.PLAYER_LAST_USED_CLASS.rawset(player.ID, classID);
    }

    /*** Class ***/

    function AddClass(team, color, skin, pos, angle, wep1, ammo1, wep2, ammo2, wep3, ammo3) {
        Classes.push(::SpawnSelectorClass(team, color, skin, pos, angle, wep1, ammo1, wep2, ammo2, wep3, ammo3));
    }

    function DisplayNextClass(player) {
        if(Players.rawin(player.ID)) {
            local playerSelectorInstance = Players.rawget(player.ID);
            local nextClassID = playerSelectorInstance.ClassID + 1;
            if(nextClassID >= Classes.len())
                nextClassID = 0;

            playerSelectorInstance.DisplayClass(Classes[nextClassID]);
        }
    }

    function DisplayPreviousClass(player) {
         if(Players.rawin(player.ID)) {
            local playerSelectorInstance = Players.rawget(player.ID);
            local previousClassID = playerSelectorInstance.ClassID - 1;
            if(previousClassID < 0)
                previousClassID = Classes.len()-1;

            playerSelectorInstance.DisplayClass(Classes[previousClassID]);
        }
    }
}

class SpawnSelectorClass {
    ID      = null;
    Team    = null;
    Color   = null;
    Skin    = null;
    Pos     = null;
    Angle   = null;
    Wep1    = null;
    Wep2    = null;
    Wep3    = null;
    Ammo1   = null;
    Ammo2   = null;
    Ammo3   = null;

    constructor(team, color, skin, pos, angle, wep1, ammo1, wep2, ammo2, wep3, ammo3) {
        ID      = ::__SPAWN_SELECTOR_STATICS__.CLASS_ID;
        Team    = team;
        Color   = color;
        Skin    = skin;
        Pos     = pos;
        Angle   = angle;
        Wep1    = wep1;
        Wep2    = wep2;
        Wep3    = wep3;
        Ammo1   = ammo1;
        Ammo2   = ammo2;
        Ammo3   = ammo3;

        ::__SPAWN_SELECTOR_STATICS__.CLASS_ID++;
    }

    function Apply(player) {
        player.Skin     = Skin;
        player.Team     = Team;
        player.Color    = Color;

        player.StripWeapons();
        player.SetWeapon(Wep1, Ammo1);
        player.SetWeapon(Wep2, Ammo2);
        player.SetWeapon(Wep3, Ammo3);

        player.Pos = Pos;
        player.Angle = Angle;
       
        player.RestoreCamera();
    }
}

class SpawnSelectorPlayer {
    Player  = null;
    ClassID = null;

    constructor(playerInstance, classInstance = null) {
        Player = playerInstance;
        if(classInstance)
            DisplayClass(classInstance);       
    }

    function destroy() {
        Player  = null;
        ClassID = null;
    }

    function DisplayClass(classInstance) {
        ClassID = classInstance.ID;
       
        Player.Skin = classInstance.Skin;
        Player.Team = classInstance.Team;
        Player.Color = classInstance.Color;
        Player.StripWeapons();
        Player.SetWeapon(classInstance.Wep1, classInstance.Ammo1); // We just need to display the first weapon only for now

        ::SpawnSelector.PlayerRequestClass.Consume(Player, classInstance);
    }
}
[/noae][/noae][/noae][/noae][/noae]

— Usage:
[noae][noae][noae]SqCore.On().ScriptLoaded.Connect(this, function() {
    CustomSpawnSelection <- SpawnSelector();
    CustomSpawnSelection.AddClass(1, Color3( 100, 126, 255 ), 62, ::Vector3(0, 0, 10), 1, 20, 999, 9, 250, 18, 500);
    CustomSpawnSelection.AddClass(2, Color3( 255, 255, 0 ), 61, ::Vector3(0, 0, 10), 1, 20, 999, 9, 250, 18, 500);
});

SqCore.On().PlayerRequestClass.Connect(this, function(player, ...) {
    player.Spawn();
});

SqCore.On().PlayerSpawn.Connect(this, function(player) {
    local lastClassID = (__SPAWN_SELECTOR_STATICS__.PLAYER_LAST_USED_CLASS.rawin(player.ID) ? __SPAWN_SELECTOR_STATICS__.PLAYER_LAST_USED_CLASS.rawget(player.ID) : 0);
    CustomSpawnSelection.AddPlayer(player, lastClassID);
});

SqCore.On().PlayerKeyPress.Connect(this, function(player, key) {
    if(CustomSpawnSelection.IsActiveForPlayer(player)) {
        switch(key) {
            case SpawnSelector.SPAWN_SELECTION_KEYS.SELECTION_CTRL:
            case SpawnSelector.SPAWN_SELECTION_KEYS.SELECTION_LMB:
            case SpawnSelector.SPAWN_SELECTION_KEYS.SELECTION_NUM0:
                CustomSpawnSelection.RequestSpawn(player);
            break;

            case SpawnSelector.SPAWN_SELECTION_KEYS.ARROW_LEFT:
                CustomSpawnSelection.DisplayNextClass(player);
            break;

            case SpawnSelector.SPAWN_SELECTION_KEYS.ARROW_RIGHT:
                CustomSpawnSelection.DisplayPreviousClass(player);
            break;
        }
    }
});

function SpawnSelector_DiscardPlayer(...) {
    if(CustomSpawnSelection.IsActiveForPlayer(vargv[0])) {
        CustomSpawnSelection.RemovePlayer(vargv[0]);
    }

   if(vargv.len() == 3) { // PlayerDestroyed
        __SPAWN_SELECTOR_STATICS__.PLAYER_LAST_USED_CLASS.rawdelete(vargv[0].ID);
    }
}

SqCore.On().PlayerKilled.Connect(SpawnSelector_DiscardPlayer);
SqCore.On().PlayerWasted.Connect(SpawnSelector_DiscardPlayer);
SqCore.On().PlayerDestroyed.Connect(SpawnSelector_DiscardPlayer);
[/noae][/noae][/noae]

— Methods:
  • SpawnSelector::Constructor(optional: Camera Data); — Camera Data is a table: {position = Vector3(...), lookAt = Vector3(...), playerPosition = Vector3(...)}
  • SpawnSelector::SetCameraPosition(Vector3 position)
  • SpawnSelector::SetCameraLookAt(Vector3 lookAt)
  • SpawnSelector::SetCameraPlayerPosition(Vector3 playerPosition)
  • SpawnSelector::AddClass(...); — Follows the same parameters as the SqMod default

These are the custom signals that you can use with this
[noae][noae][noae][noae][noae]/***
    Signals
***/

SqSignal("PlayerRequestClass").Connect(function(player, classInstance) {
    printf("PlayerRequestClass: %d", classInstance.ID);
});

SqSignal("PlayerRequestSpawn").Connect(function(player, classInstance) {
    /***
        return true — Stop the handler execution here, Don't execute any later connected handlers to this signal
        return false — Returns false to the .Consume(...) method, treat it similar to return 0 in official plugin
    ***/

    print("PlayerRequestSpawn");
    return true;
});

SqSignal("PlayerSpawn").Connect(function(player) {
    print("PlayerSpawn");
});
[/noae][/noae][/noae][/noae][/noae][/noae]

Forgive me if the code is ugly :P I just did it for a friend of mine cuz he was busy.

VK.Angel.OfDeath

Hello, sorry if this is out of topic. I noticed a similar issue back when rel006 was released. I am not sure if it's in anyway related to this issue, but I think it might be worth mentioning.

Back when I switched to rel006, a player with a very limited internet reported to me that his download rate increased the longer he stayed on our spawn screen. I did some investigation and found out that it was caused by pickups. After removing a few pickups in the field of view of the spawn screen camera, the issue got resolved and the download rate stopped increasing.

Either way, I haven't noticed any memory leakages from this on my own system. How did you guys test this issue? Did it occur on a blank server as well? Is it an issue with both Linux and Windows servers?

Xmair

Quote from: VK.Angel.OfDeath on Apr 05, 2020, 11:29 AMHello, sorry if this is out of topic. I noticed a similar issue back when rel006 was released. I am not sure if it's in anyway related to this issue, but I think it might be worth mentioning.

Back when I switched to rel006, a player with a very limited internet reported to me that his download rate increased the longer he stayed on our spawn screen. I did some investigation and found out that it was caused by pickups. After removing a few pickups in the field of view of the spawn screen camera, the issue got resolved and the download rate stopped increasing.

Either way, I haven't noticed any memory leakages from this on my own system. How did you guys test this issue? Did it occur on a blank server as well? Is it an issue with both Linux and Windows servers?

Afiak, Stormeus confirmed that staying on spawn screen for long times sends a ton of packets for unknown reasons. This issue occurs on both, Linux and Windows servers.

Credits to Boystang!

VU Full Member | VCDC 6 Coordinator & Scripter | EG A/D Contributor | Developer of VCCNR | Developer of KTB | Ex-Scripter of EAD

PerikiyoXD

#3
Seby, s19 and I have tested this.

We've tried connecting all of us multiple instances of VCMP client to s19's server (19323 Racing) up to 32 clients.

The clients were run in windowed mode so the connection wouldn't break, also not having minimized window.

We've run this test for ~20 minutes.

We added pickups to the spawn screen, nothing happened.

We've tried to mess around with some players ingame and others at spawn screen, nothing happened.

I also, tried this myself connecting with 16 clients on a local server. No proof of this "leak" appearing on our servers.

We tested this supossed leak with:
  • Linux, 04rel006, 64 bits server executable
  • Windows, 04rel006, 64 bits server executable

DizzasTeR

#4
Its weird, on one side we have people who see memory spike up, and then on the other we had those who say there's nothing happening, anyhow we'll see how it goes.

:edit: Also added a small table value clear code that I forgot, first post updated (Usage section)

NewK

It would seem this (memory leak) can't be reproduced on a blank server with an empty script, which makes me think this is somehow related to some vcmp entity (player, vehicle, pickup, sphere, checkpoint, etc...) and sitting on the spawn screen. 

If anyone can reproduce it on a blank script let us know.

Anish87

Quote from: NewK on Apr 08, 2020, 08:46 PMIt would seem this (memory leak) can't be reproduced on a blank server with an empty script, which makes me think this is somehow related to some vcmp entity (player, vehicle, pickup, sphere, checkpoint, etc...) and sitting on the spawn screen. 

If anyone can reproduce it on a blank script let us know.
Well, I guess I am bumping into the topic unnecessarily but the memory leak happens only when there is other stuff in the server like when an entity's data is being saved in arrays/tables/classes...

DizzasTeR

The memory leaks have been fixed in the last server update

.

Quote from: Anish87 on Dec 27, 2020, 07:30 AM
Quote from: NewK on Apr 08, 2020, 08:46 PMIt would seem this (memory leak) can't be reproduced on a blank server with an empty script, which makes me think this is somehow related to some vcmp entity (player, vehicle, pickup, sphere, checkpoint, etc...) and sitting on the spawn screen. 

If anyone can reproduce it on a blank script let us know.
Well, I guess I am bumping into the topic unnecessarily but the memory leak happens only when there is other stuff in the server like when an entity's data is being saved in arrays/tables/classes...

This was beyond my scope to address (i assume. i didn't even bother).
.