Jump to content

Dedicated Server with bash, PHP with help from the API


Recommended Posts

Hi Survivors,

 

First, I would like to say that I am neither a developer in the true sense of the word, nor a fully trained administrator or anything like that. Everything in the following I have taught myself, and that only for the reason that it is fun for me.

So if you discover technical gaps or find out that things are done differently nowadays, feel free to contact me, because I like to take advice that helps me to have even more fun and joy with this project.


But now back to the topic.

 

My plan is, that I show you ( because I enjoy it ) in this post regularly some nice things, and how I implemented them with PHP, bash, XML, ... this post should be the introduction.
If you have any questions, of course feel free to ask, that's why we are here. 😇

 

What is it actually about?

Nowadays, running a server is basically very simple. Download, adjust the configuration and maybe a few mods for optics and special items purely, start and ready. But it can be so much better and with so much more functionality, because 7D2D can do great things with a few small tools and the API.

 

What can a server look like?
 

Operating system: Linux ( I prefer Ubuntu ).

This includes then for the operation in the operating system installed

  • PHP (best in a 7 version) in the client version (PHP-CLI) 
  • xmlstarlet (to read or edit XML files in bash)
  • LinuxGSMhttps://linuxgsm.com/servers/sdtdserver/ ) great tool to install, update, run the server

 

Kernel-Mods

Besides all the mods that make the game itself prettier, there are mods that drastically expand the range of functions and should therefore be mandatory.

 

Allocs Server-Fixes and Live-Maphttps://7dtd.illy.bz/wiki/Server fixes#Download )

Without this mod(s) basically nothing would work, because without the API all efforts with PHP or bash would be waste. So the API is the core for everything else.

 

CPM - CSMM Patron's Modhttps://docs.csmm.app/en/CPM/  )

This mod provides a number of useful functions that can be used in the console. Among other things, teleports, spawning of hordes or functions that allow the player to pack items in the backpack and much more.

7dtd-ServerToolshttps://github.com/dmustanger/7dtd-ServerTools )

Similar to CPM, the server tools provide a lot of functionality to help you run the server and make it even cooler.

From my point of view, the biggest strength of the server tools is the management of the zones via live XMLs. Moreover, it is OpenSource.

 

There are certainly a few more mods in this direction, but with these three I have had the best experience so far.

 

All beginnings are difficult 😢

To run the first command with PHP or bash it is necessary to set one important thing. Namely, the API token.

This must be done in the webpermissions.xml which is usually located in the main directory of the saves.

 

Here is an example:

<?xml version="1.0" encoding="UTF-8"?>
<webpermissions>
	<admintokens>
		<token name="myadmintoken" token="verysecrettoken" permission_level="0"/>
	</admintokens>
	<permissions>
		<!-- permissions for the map -->
	</permissions>
</webpermissions>

This token opens up endless possibilities with the help of the API. At this point, it is important to choose the right 'permission_level', because some things only work correctly with level = 0 (which is the admin level).

 

Once that is set, you immediately have the ability to send commands through the API using Curl, a browser, or even Postman.

 

Example in bash for displaying the help:

curl http://<serverip>:<mapport>/api/executeconsolecommand?adminuser=myadmintoken&admintoken=verysecrettoken&raw=true&command=help

 

Once that is done, and it works, the rest is pretty easy to implement in vanilla PHP.

 

Simple example in PHP:

$url = 'http://<serverip>:<mapport>/api/executeconsolecommand?adminuser=myadmintoken&admintoken=verysecrettoken&raw=true&command=' . urlencode('help');
$ch = curl_init();
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_URL => $url]);
$result = curl_exec($ch);
curl_close($ch);

 

Important at this point is to make a 'urlencode' for the actual command, because many commands have additional parameters and already a space can become a problem.

 

Now you can do almost everything you want or what is possible with console commands.


This includes, for example (I will describe all of them little by little in following posts):

  • spawn zombies or other entities (also in hordes)
  • teleport players
  • give items to players
  • apply buffs and also query them
  • show tooltips
  • play sounds
  • send messages to the chat
  • monitor and query the log and also the chat
  • determine player positions
  • open up windows
  • execute twitch commands
  • create own events
  • show infos in a ticker way with tooltips
  • give quests to the player
  • watch server events like:
    • join/left
    • die
    • kill
    • levelup


but in addition you can do other great things with PHP like creating XML files that are otherwise very complex.

Like for example entitygroups or tooltips.

 

This should be enough to get you started. As the next post, I will try to explain how to make an own event, similar to the Horde Night.

 

CU and gladly asks questions. You are welcome ...

Link to comment
Share on other sites

How to create an event like the Horde Night?

 

to create an event you need something that takes the component time from you. So a process that makes sure that in certain intervals is queried how the state of the game is and executes commands based on this information.

Since my example is in the area of Linux, 'systemd' is a good choice. A construct, which makes it possible to start certain commands exactly to the second.

Such a service is divided into two parts. Once the start component (the service) and once the time component.

 

Here is link to a good description for systemd (german): https://wiki.archlinux.de/title/Systemd/Timers

But I am sure that there are also good tutorials in English. Just google a little bit.

 

Here is an example of both components:

 

SERVICE

[Unit]
Description=start event check
[Service]
ExecStart=/<path_to_your_script>/7d2d.sh events

 

TIMER

[Unit]
Description=7d2d event check every 10 seconds
[Timer]
OnBootSec=10
OnUnitActiveSec=10
AccuracySec=1
[Install]
WantedBy=timers.target


What does this do?

If activated, this service starts the bash file 7d2d.sh with the parameter events. This happens every 10 seconds.

Of course, you can make this period shorter, but for an event this is not necessary.

 

In this '.sh' file I can of course do everything that a bash can do. Among other things of course also PHP start.

 

Example:

#!/bin/bash
php /<path_to_php_script>/startup.php $@

 

This PHP script can query or trigger anything that the API can do and of course execute commands that get you the necessary information, and also start actions.

 

Here is an example of how a horde night can look like:

 

public function event()
{
    if (EVENT_BLOODMOON) {
        initEvent(get_class($this));
        $locations = getPlayersLocation();
        $maxZombies = ((BLOOD_MOON_ENEMY_COUNT * ePLAYERS) > MAX_SPAWNED_ZOMBIES) ? MAX_SPAWNED_ZOMBIES : BLOOD_MOON_ENEMY_COUNT * ePLAYERS;
        $maxZombiesForPlayer = intval($maxZombies / ePLAYERS);
        $allZombies = getEntities();
        $zombiesInRadius = [];
        if (count($allZombies) < MAX_SPAWNED_ZOMBIES) {
            foreach ($locations as $player) {
                $zombiesInPlayerRadius = getCustomEntities($player['position'], 'Zombie', LAND_CLAIM_SIZE * 3);
                $playerAtPlayer = count(getCustomEntities($player['position'], 'Player', LAND_CLAIM_SIZE * 3));
                $zombiesInRadius += $zombiesInPlayerRadius;
                $zombiesAtPlayer = ($playerAtPlayer > 1) ? intval(count($zombiesInPlayerRadius) / $playerAtPlayer) : count($zombiesInPlayerRadius);
                if (($maxZombiesForPlayer - $zombiesAtPlayer) > 0) {
                    targetedHorde($player[STEAM_ID], ($maxZombiesForPlayer - $zombiesAtPlayer));
                }
            }
        }
        cleanupEntities($allZombies, $zombiesInRadius);
    } else {
        endEvent(get_class($this));
    }
}

 

Explanation lines by lines of what is happening there.

 

EVENT_BLOODMOON => true/false 

Here, simply the time on the server is queried and checked whether it corresponds to the time in which the event should run.

The time query is very simple via the API:

curl http://<serverip>:<mapport>/api/getstats

 

The result is a JSON which can be queried in PHP without any problems:

{
 gametime: {
    days: 517,
    hours: 23,
    minutes: 3
  },
  players: 2,
  hostiles: 10,
  animals: 5
}

 

So in this case, you only need to determine if it is the 7/8 day and the time is between 10pm and 4am.

 

initEvent

Here, simply a BUFF is set, which indicates that the event is started. Setting a buff is quite simple. I used the command:

buffplayer Steam_12321313212131 eventBuffName

 

getPlayerLocations()

Very important to determine what is happening around the player's position during the event. This query also returns a JSON which can be saved for later use:

curl http://<serverip>:<mapport>/api/getplayerslocation

JSON:

[
	{
		steamid: "Steam_76561198075097605",
		name: "dwarfmaster",
		online: true,
		position: {
			x: 3073,
			y: 37,
			z: 818
		}
	}
]

The variable $locations now contains all positions of the players.

 

maxZombies

This is just simple math to calculate how many zombies should be spawned. You just have to play around a bit

 

getEntities()

This is a little more complex, because here the goal is to determine everything that is already spawned and what state it is in (dead/alive).

For this I use the command: 'le' which returns a result that looks something like this:

1. id=415881, [type=EntityZombie, name=zombieSoldierFeral, id=415881], pos=(3063.3, 37.6, 819.7), rot=(0.0, 21.8, 0.0), lifetime=float.Max, remote=False, dead=False, health=232
2. id=415880, [type=EntityZombie, name=zombieSkateboarder, id=415880], pos=(3054.8, 37.0, 817.5), rot=(0.0, 270.0, 0.0), lifetime=float.Max, remote=False, dead=False, health=221
3. id=415879, [type=EntityZombie, name=zombieSteve, id=415879], pos=(3068.9, 37.1, 815.6), rot=(0.0, 113.1, 0.0), lifetime=float.Max, remote=False, dead=False, health=100
4. id=415878, [type=EntityZombie, name=zombieSkateboarderRadiated, id=415878], pos=(3063.9, 37.1, 817.1), rot=(0.0, 117.8, 0.0), lifetime=float.Max, remote=False, dead=False, health=785
5. id=415877, [type=EntityZombie, name=zombieTomClark, id=415877], pos=(3070.2, 37.1, 816.5), rot=(0.0, 98.6, 0.0), lifetime=float.Max, remote=False, dead=False, health=141
6. id=415876, [type=EntityZombie, name=zombieSteveCrawlerFeral, id=415876], pos=(3062.9, 37.0, 817.2), rot=(0.0, 266.0, 0.0), lifetime=float.Max, remote=False, dead=False, health=144
7. id=415875, [type=EntityZombie, name=zombieSteveRadiated, id=415875], pos=(3062.8, 37.1, 821.6), rot=(0.0, 27.0, 0.0), lifetime=float.Max, remote=False, dead=False, health=505
8. id=415874, [type=EntityZombie, name=zombieSteveCrawler, id=415874], pos=(3063.6, 37.0, 819.6), rot=(0.0, 93.1, 0.0), lifetime=float.Max, remote=False, dead=False, health=74
9. id=616, [type=EntityPlayer, name=dwarfmaster, id=616], pos=(3073.3, 37.2, 818.0), rot=(5.6, 289.7, 0.0), lifetime=float.Max, remote=True, dead=False, health=200
Total of 9 in the game

Of course, this has to be parsed first, so that you can use it well in PHP.

This can be done well with RegEx.

 

Example:

$parts = preg_split('/(.*\. id=|, \[type=|, name=|, id=.*pos=\(|\).*dead=|, health=|, )/', $entityline);

The single parts can be put into an object or into an array. How you like it.

 

I think the next lines are almost self-explanatory, and it gets exciting again with the following command:

getCustomEntities

What happens there in the background is that it determines what entities are in a radius around the player.

This happens with the command: 'listcustomentity'.

 

Example:

listcustomentity x z radius type

x, z stand for the position of the player and radius for the radius in which the player should look.

 

The parsing is almost the same as the 'le' command.

 

With this information, we can now determine exactly how many new zombies to give the player.

We pass this to the command: 'cpm-targetedhorde'.

 

Example:

cpm-targetedhorde Steam_31213132131 15

This command from the mod CPM spawns the zombies, and these are then also set directly on the player and want to eat him. Just as it should be. 😇

 

We're pretty much done, just one more thing, and we'll have it.

 

cleanupEntities

This is really important, because every dead zombie that is lying around somewhere costs performance, and it quickly starts to lag if you don't remove these corpses.

So this command looks how many zombies have the status dead and then removes them.

This then with the command:

cpm-entityremove 123


Done. Now we have our own hordes night.

For questions, please always ask ;)

 

Link to comment
Share on other sites

Monitor and query the log and also the chat

 

Why would I want to do that?

Monitoring the log and the chat are central elements to trigger certain events in the game, or to enable players to enter things in the chat and react to them.

 

The events that can be monitored in the log are, for example. 

  • login / join
  • death of a player
  • kill (zombie, animal, player)
  • levelup

The chat can be read out and reacted to depending on how you want to read out commands. The best way to do this is with a prefix. Most often the '/' is used for this.

Example for teleporting to the home base:

PLAYERNAME: /home

 

How can I make this happen?

 

Here, too, the API helps us where we can quite easily get a number of log entries specified by us.

 

Example for getting the last 200 entries:

curl http://<serverip>:<mapport>/api/getlog?adminuser=myadmintoken&admintoken=verysecrettoken&count=-200

 

Of course, the response then just needs to be evaluated. This is also supplied with format JSON, which makes it a bit easy.

 

Example:

{
    "firstLine": 10000,
    "lastLine": 10200,
    "entries": [
        {
            "date": "2022-05-16",
            "time": "10:37:56",
            "uptime": "0",
            "msg": "Chat (from '-non-player-', entity id '-1', to 'Global'): 'SERVER': Test",
            "trace": "",
            "type": "Log"
        },
		{...},
		{...},
		{...}
	]
}

 

Simply iterate through the 'entries' array and evaluate the 'msg' accordingly. It contains exactly what we need.

 

You just have to parse such a message and check if there is a message in it or not to trigger a command.

 

Example:

$parts = preg_split('/(Chat \(from \'|\', entity id \'|\', to \'Global\'\): \'|\':)/', $entry);

To test PHP-RegEx I recommend this page here: https://www.phpliveregex.com/#tab-preg-split 

This really helped me a lot, because there is a lot to parse.

 

These individual (chat) pieces then provide you with all the information you need to start further actions:

$steamid = $parts[1];
$entityid = $parts[2];
$playername = $parts[3];
$message = $parts[4];
if (isStartingWith($message, '/')) {
    $command = substr($this->message, 1);
}

 

With the variable $command you can then trigger a teleport for example.

Here is an example for the input '/home' which triggers the command 'cpm-teleportplayerhome':

$url = 'http://<serverip>:<mapport>/api/executeconsolecommand?adminuser=myadmintoken&admintoken=verysecrettoken&raw=true&command=' . urlencode('cpm-teleportplayerhome '.$steamid);
$ch = curl_init(); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_URL => $url]); $result = curl_exec($ch); curl_close($ch);

'cpm-teleportplayerhome' teleports the player to his active bedroll  or bed.

 

That's all you need to monitor the chat input.

 

For monitoring server events, a little more is needed, because there are a few more of them. Here are a few examples of what the messages in the logs can look like and how best to parse them.

// JOIN
// Message = Player connected with ID 'Steam_12345678912345678' 'EOS_00055455dg45h45hhdd54n5dh45dh412' and IP '192.168.178.123' named 'dwarfmaster'

$parts = preg_split('/(ID \'|\' \'|\')/', $message);

$steamid = trim($parts[1]);
$playername = trim($parts[6]);
$eosid = trim($parts[2]);
$ip = trim($parts[4]);

// KILL
// Message = entityKilled: dwarfmaster (Steam_12345678912345678) killed zombie zombieFatCopRadiated with dwarfmaster-Gun

$parts = preg_split('/(.*entityKilled: |\) killed |\(| with )/', $message);
$typeParts = explode(' ', $parts[3]);

$animal = (trim($typeParts[0]) == 'animal'); // checks if entity is an animal > true/false
$entity = trim($typeParts[1]);
$with = trim($parts[4]);
$playername = trim($parts[1]);
$steamid = trim($parts[2]);

// DEATH
// Message = playerDied: dwarfmaster (Steam_12345678912345678) died @ 250 50 519

$parts = preg_split('/(playerDied: | \(|\) died @ )/', $message);
$deathLocationParts = explode(' ', $parts[3]);

$playername = trim($parts[1]);
$steamid = trim($parts[2]);
$deathLocation = ['x' => intval($deathLocationParts[0]), 'y' => intval($deathLocationParts[1]), 'z' => intval($deathLocationParts[2])];

// LEVELUP
// Message = playerLeveled: dwarfmaster (Steam_12345678912345678) made level 545 (was 544)

$parts = preg_split('/(playerLeveled: |\)| made level |\(|was )/', $this->entry['msg']);

$playername = trim($parts[1]);
$steamid = trim($parts[2]);
$newlevel = intval($parts[4]);
$oldlevel = intval($parts[6]);
				

// LEFT
// Message = Player disconnected: EntityID=616, PltfmId='Steam_12345678912345678', CrossId='EOS_00055455dg45h45hhdd54n5dh45dh412', OwnerID='Steam_12345678912345678', PlayerName='dwarfmaster'

$parts = preg_split('/(\')/', $message);

$steamid = trim($parts[1]);
$playername = trim($parts[7]);

 

With all this information, of course, you can do a lot. Whether that's saving for stats, or showing players in chat who just came onto the server, or even which player just died or killed which zombie.

 

Done. Now we have monitoring of the chat and server events.

Link to comment
Share on other sites

On the topic of monitoring logs, allocs also has a log push interface at http://<serverip>:8082/sse/log which will push changes to you as it goes, rather than polling.

 

It puts lighter load on the server and you generally get the log pushed even quicker than polling.

 

This does need a ServerSentEvents (also known as EventSource) connection from the client (your PHP script).

Edited by pharrisee (see edit history)
Link to comment
Share on other sites

Thanks to @pharrisee for the tip with the SSE!


That has of course changed some procedure, how to monitor the log or the chat best.

Immediately implemented it, of course, which cost me a little time, but it was absolutely worth it.

The big advantage of SSE is the low load and the synchronous processing of the inputs or the reception of certain events.

This means that inputs in the chat take place without any delay and can be reacted to immediately. The same is true for events like a kill, which is registered immediately without taking a few seconds.

The implementation in PHP for this is in principle quite simple, if you have dealt with the topic a little bit.

I used an external library for this (again, thanks to @pharrisee for the recommendation) 
https://github.com/eislambey/php-eventsource
After including the library, you have to start a process that does the monitoring. This can look like the following:

 

$es = new EventSource('http://<serverip>:<mapport>/sse/log?adminuser=myadmintoken&admintoken=verysecrettoken');
$es->onMessage(function (Event $event) {
	new Logdispatch($event->data);
});
$es->connect();

Important!

For this, of course, the server must already be running, otherwise the connection will not work. So it may only be started when the live map is accessible.
 

Behind the class 'Logdispatch' the actual parsing of the log entries takes place, of which there are quite a lot. I decided to do a pre-sorting first, so that not every message is processed and if then also from the correct endpoint.

It then looks like this:
 

class Logdispatch
{
    /**
     * @throws Exception
     */
    public function __construct($entry)
    {
        $entry = json_decode($entry, true);
        if (isset($entry['type']) && $entry['type'] == 'Log') {
            if (isChat($entry['msg'])) {
                new Chat(new ChatEntry($entry));
            } else if (isServerEvent($entry['msg'])) {
                new Serverevents(new ServerEventEntry($entry));
            }
        }
    }
}

What happens here is that only entries of the type 'Log' are processed, because only there is the information interesting for us.

With 'isChat()' and with 'isServerEvent()' we sort out if the arrived log entry is a chat or a server event.

 

Here is an example:

function isChat($message): bool
{
    if (isStartingWith($message, 'Chat') && !isContaining($message, 'non-player')) {
        $return = true;
    }
    return $return ?? false;
}

This identifies all chat entries and at the same time filters out messages from the server ('non-player').

 

With this information we can then trigger actions like in my previous POST, but much more synchronously. 

 

What we don't get at this point, however, is the problem of having events that are repetitive and precise in time. Like for example every 10 seconds to check if there is a blood moon and possibly send new zombies. For this I have already described that you can create a systemd service, but there are other ways.

 

This one is a little bit like getting a text message directly from hell and actually hurts just looking at it, but it works stable and can be used in the user context:

 

user@ubuntu:~$ crontab -e

SDTD="/<path_to_your_script>/7d2d.sh"
* * * * * ${SDTD}
* * * * * ( sleep 10; ${SDTD})
* * * * * ( sleep 20; ${SDTD})
* * * * * ( sleep 30; ${SDTD})
* * * * * ( sleep 40; ${SDTD})
* * * * * ( sleep 50; ${SDTD})

 

But also a great solution is the Dispatcher from @pharrisee which he once quickly developed. ;) 

You can find it at: https://github.com/pharrisee/dispatcher

 

The big advantage here, of course, is that this runs on both Linux and Windows, since it's written in golang.

 

Edited by dwarfmaster1974 (see edit history)
Link to comment
Share on other sites

As I wrote in the first post, you can really do quite a lot. But I also promised to explain everything once.

Let's take a look at the list and check it off:

  • spawn zombies or other entities (also in hordes) ✔️
  • teleport players ✔️
  • give items to players
  • apply buffs and also query them
  • show tooltips
  • play sounds
  • send messages to the chat
  • monitor and query the log and also the chat ✔️
  • determine player positions
  • open up windows
  • execute twitch commands
  • create own events ✔️
  • show infos in a ticker way with tooltips
  • give quests to the player
  • watch server events ✔️

 

So there are still a few things to check off about.

 

Give items to players, play sound, show tooltips in a ticker way, ...

There is always the need to simply execute commands on the console, and there are the commands that are available in the server managers (such as CPM or server tools) help. Of course, it is helpful to build a function that helps you.

 

Example:

function executeConsoleCommand($command)
{
    $command = is_array($command) ? implode(' ', $command) : $command;
    $url = 'http://<serverip>:<mapport>/api/executeconsolecommand?adminuser=myadmintoken&admintoken=verysecrettoken&raw=true&command=' . urlencode($command);
    $ch = curl_init();
    curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_URL => $url]);
    $return = curl_exec($ch);
    curl_close($ch);
    return $return ?? null;
}

 

With this, you can do the following nice things with the help of commands that are available in CPM or similar:

// give item to players
executeConsoleCommand(['cpm-giveplus', $steamid, $itemname, $amount, $quality, $usages]);

// play sounds
executeConsoleCommand(['playsound', $steamid, $soundname]);

// show tooltips to evry online player
executeConsoleCommand(['cpm-tooltip', 'all', $nameOfTheToolTipBuff]);


But sometimes it is necessary to run some things that do not work on the server, but even here there is a great solution.

Namely the execution on the client itself. For this in CPM there is the command 'eoc' (executeonclient) which helps you there.

The cool thing you can do is for example open a game window on the client to show it 'static' information there.

 

What does it take? A few things. First, of course, we need a window, we create that in windows.xml:

<window name="infoWindow" anchor="CenterCenter" pos="0,0" width="150" height="150">
	<rect name="content" pos="0,0">
		<label depth="4" name="name" width="50" height="50" color="255,255,255" pivot="center" justify="center" text="THE INFO" font_size="70"/>
	</rect>
</window>

and a 'window_group in the 'xui.xml'.

<window_group name="INFOGROUP">
	<window name="infoWindow"/>
</window_group>

 

then of course we need a function that facilitates the execution on the client:

function executeOnClient($steamid, $command): void
{
    $command = is_array($command) ? implode(' ', $command) : $command;
    executeConsoleCommand(['eoc', $steamid, '"' . $command . '"']);
}

then the rest is not bad at all:

// open a window
executeOnClient($steamid, ['xui', 'open', 'INFOGROUP', '1', '1']);

// close a window
executeOnClient($steamid, ['xui', 'close', 'INFOGROUP', '1']);

 

That was it already ... I think I have now written everything once in detail and am very happy about feedback.

If someone has still more ideas, what one can make in such a way, I am pleased about it also very much.

 

Best regards from the north of Germany

dwarfmaster

 

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...