https://forums.funcom.com/

Conan Exiles 专用服务器启动器

跨地图传送

2305969565 Amunets Server Transfer - v1.1.18
原作者提供的使用说明
服务器传输示例图.png
视频教程 视频为英文,听不懂没关系,可以参考视频中的流程和本教程相互印证

配置web服务器

1、到 files.arianchen.de 网站下载最新的网站压缩包(如 amunet-server-transfer_1_1_1a.zip ),将压缩包文件解压后上传web服务器,注意web服务器php版本设置为7.4版本

2、打开cluster.ini文件,将里面的配置文件改为对应服务器的ip、端口和密码
2023-05-28T05:15:14.png

3、打开web服务器的测试页面,查看是否正确配置

http://<webserver>/<pfad>/cluster.php?cmd=test

// 如网站地址为www.darktool.cn,且cluster.php等文件存放于web服务器根目录,则输入如下地址到浏览器地址栏查看
// http://www.darktool.cn/cluster.php?cmd=test

2023-05-28T05:35:28.png

常见错误

  • Authentication failed 给定的 RCON 密码错误
  • Connection refused 指定的 RCON 连接错误,或防火墙引起问题
  • Permission denied 可能是网络服务器(出于安全原因)不允许连接到外部
  • Couldn't find the command: listplayers. Try "help" - der Gameserver bootet grade erst und ist noch nicht soweit.

易产生歧义错误

  • 2-Way-Test: failed 该错误不表明web服务器配置错误,而是我们未在游戏中设置游戏服务器连接到web服务器导致

4、调整 cluster.php 代码,实现自己需要的效果,如:

<?php
// if ($_SERVER['REQUEST_METHOD'] === 'POST') {
//     // log
//     $chatlog = fopen("postChat.log", "a");
//     if ($chatlog) {
//         fwrite($chatlog, date("Y-m-d H:i:s") . " " . $_SERVER["REQUEST_URI"]);
//         fwrite($chatlog, " data: [");
//         foreach ($_POST as $key => $value) {
//             fwrite($chatlog, $key. ": " .$value. ", ");
//         }
//         fwrite($chatlog, "]\n");
//         fclose($chatlog);
//     }
//     returnSuccess();
//     return;
// } else {
//     // log
//     $chatlog = fopen("chat.log", "a");
//     if ($chatlog) {
//         fwrite($chatlog, date("Y-m-d H:i:s") . " " . $_SERVER["REQUEST_URI"] . "\n");
//         fclose($chatlog);
//     }
// }
// cluster.php for Amunets Server Transfer v1.1.1+
$backup_multichar = false;
$backup_rotate = true;

set_time_limit(300);
include_once('rcon.php');
//include_once('continue.php');
//include_once('primary.php');
//include_once('pippi.php');
//include_once('votes.php');

function returnSuccess() {
    returnError(204, '');
    //exit('{"ManifestFileVersion":"000000000000", "bIsFileData":false, "AppID":"000000000000", "AppNameString":"", "BuildVersionString":"", "LaunchExeString":"", "LaunchCommand":"", "PrereqIds":[], "PrereqName":"", "PrereqPath":"", "PrereqArgs":"", "FileManifestList":[], "ChunkHashList":{}, "ChunkShaList":{}, "DataGroupList":{}, "ChunkFilesizeList":{}, "CustomFields":{}}');
}

function returnError($code, $message) {
    http_response_code($code);
    exit($message);
}

function get($name) {
    return isset($_GET[$name]) ? $_GET[$name] : '';
}

// rcon protocol defines a limit of 4096 bytes per packet, however conan seems to support up to 10240 bytes per packet.
$limit = 10138;//3994;

$ini_array = parse_ini_file('cluster.ini', TRUE);

// these should be a given
$server_id = get('srv');
$command = get('cmd');
$funcom_id = get('fid');
$parameter = get('prm');

switch ($command) {
    case 'test':
        echo "<html><body>";
        echo '<h1>Amunet Server Transfer - v1.1.1</h1>';

        // test webserver stuff...
        echo '<h2>webserver test</h2>';
        echo '<ul>';

        echo '<li><p>fopen() ';
        $filename = 'filesystem.test';
        $handle = @fopen($filename, 'w');
        if ($handle) {
            echo '<strong style="color: green;">works</strong>.';
            fclose($handle);
            unlink($filename);
        } else {
            echo '<strong style="color: red;">failed</strong>!<br/>FATAL - make sure php has write access to the file system.</p>';
        }
        echo '</li></p>';

        echo '<li><p>json_decode() ';
        if (function_exists('json_decode')) {
            echo '<strong style="color: green;">found</strong>.';
        } else {
            echo '<strong style="color: orange;">not found</strong>!<br/>WARNING - transfers SHOULD work, but without JSON checks - make sure php is up2date and the json module is enabled.</p>';
        }
        echo '</li></p>';
        echo '</ul>';

        // test rcon access to servers
        echo '<h2>cluster servers</h2>';
        echo '<ul>';
        foreach ($ini_array as $key => $value) {
            echo '<li><h3>'.$key.'</h3>';
            $rcon = new rcon($value['pass'], $value['host'], $value['port']);
            if ($rcon->connected) {
                echo '<p>RCON: connected!</p>';

                // ping-pong test
                echo '<p>2-Way-Test: ';
                $filename = 'ping-'.$key.'.test';
                if (file_exists($filename))
                    unlink($filename);

                $result = $rcon->send('ast ping');
                if ($result == 'ping done.') {
                    // ping should be done now, but wait a second just to be sure
                    if (!file_exists($filename))
                        sleep(1);
                    if (!file_exists($filename))
                        sleep(1);

                    if (file_exists($filename)) {
                        echo '<strong style="color: green;">good</strong>.';
                        unlink($filename);
                    } else {
                        echo '<strong style="color: red;">failed</strong>.';
                    }
                } else {
                    // possibly not ingame or old mod version
                    echo '<strong style="color: orange;">not available</strong>.';
                }
                echo '</p>';

                // list players
                echo '<pre style="border-style: double;"><code>'.$rcon->send('listplayers').'</code></pre>';

            }
            echo '</li>';
        }
        echo '</ul>';

        // list files
        echo '<h2>character files</h2>';
        echo '<ul>';
        $files = scandir('.');
        foreach ($files as $filename) {
            if (substr($filename, -5) == '.json') {
                echo '<li>'.$filename.'</li>';
            }
        }
        echo '</ul>';

        echo "</body></html>";
        break;

    case 'chat':
        // in case of chat check if valid server provided
        if (!isset($ini_array[$server_id]))
            returnError(400, 'cannot chat - unknown source server "'.$server_id.'"');

        // fetch config details
        $config = $ini_array[$server_id];

        // try to connect to rcon
        $rcon = new rcon($config['pass'], $config['host'], $config['port']);
        if (!$rcon->connected)
            returnError(400, 'cannot chat - rcon connection failed');

        // convert from UTF-16 to UTF-8
        $message = iconv("UTF-16BE", "UTF-8", $parameter);

        
        // log
        // 检查目录是否存在
        $dir = date("Y-m");
        if (!is_dir($dir)) {
            // 目录不存在,尝试创建目录
            mkdir($dir, 0755, true);
        }
        $chatlog = fopen($dir . "/" . date("Y-m-d") . "-chat.log", "a");
        if ($chatlog) {
            fwrite($chatlog, date("Y-m-d H:i:s") . " " . urldecode($parameter) . "\n");
            fclose($chatlog);
        }
        

        // now send to every other server
        foreach ($ini_array as $key => $value) {
            if ($key != $server_id) {
                $rcon = new rcon($value['pass'], $value['host'], $value['port']);
                if ($rcon->connected) {
                    $result = $rcon->send('ast chat "global" "'.$parameter.'"');
                }
            }
        }

        returnSuccess();
        break;

    case 'export':
        // in case of export check if valid servers provided
        if (!isset($ini_array[$server_id]))
            returnError(400, 'cannot export - unknown source server "'.$server_id.'"');

        if (!isset($ini_array[$parameter]))
            returnError(400, 'cannot export - unknown destination server "'.$parameter.'"');

        // fetch config details
        $config = $ini_array[$server_id];
        $config2 = $ini_array[$parameter];

        // try to connect to rcon
        $rcon = new rcon($config['pass'], $config['host'], $config['port']);
        if (!$rcon->connected)
            returnError(400, 'cannot export - rcon connection failed');

        // request export
        $result = $rcon->send('ast export "'.$funcom_id.'"');
        if ($result != 'export done.')
            returnError(400, 'cannot export - rcon reply "'.$result.'"');

        // export to the buffer is done - now read until we receive no data.
        $json_string = '';
        do {
            $result = $rcon->send('ast read "'.$funcom_id.'"');
            $json_string = $json_string.$result;
        } while ($result != ' ');

        // make sure json is valid!
        if (function_exists('json_decode')) {
            $json = json_decode($json_string, true);
            if (!isset($json))
                returnError(400, 'cannot export - invalid json received');

            if ($backup_multichar) {
                // char name-based backup
                $filename = 'multichar_'.$funcom_id.'_'.$json["name"].'.json';
                $handle = fopen($filename, 'w');
                fwrite($handle, $json_string);
                fclose($handle);
            }
        } else {
            if (substr($json_string, 0, 1) != '{' || substr($json_string, -2, 1) != '}')
                returnError(400, 'cannot export - invalid json received');
        }

        // got valid json, save to file
        $filename = 'export_'.$funcom_id.'.json';
        $handle = fopen($filename, 'w');
        fwrite($handle, $json_string);
        fclose($handle);

        // MOD exports begin
        // MOD exports end

        // remove the player and execute open command
        $rcon->send('ast remove "'.$funcom_id.'"');
        $rcon->send('ast exec "'.$funcom_id.'" "open '.$config2['open']).'"';

        returnSuccess();
        break;

    case 'import':
        // in case of import check if valid server provided
        if (!isset($ini_array[$server_id]))
            returnError(400, 'cannot import - unknown server "'.$server_id.'"');

        // default value
        $votes_result = -1;

        // MOD check begin
        // MOD check end

        // check if we got any file to import
        $filename = 'export_'.$funcom_id.'.json';
        if (!file_exists($filename) && $votes_result != 1)
            returnSuccess();

        // fetch config details
        $config = $ini_array[$server_id];

        // try to connect to rcon
        $rcon = new rcon($config['pass'], $config['host'], $config['port']);
        if (!$rcon->connected)
            returnError(400, 'cannot import - rcon connection failed');

        // if we got any file to import
        if (file_exists($filename)) {
            // try to read the file
            $handle = fopen($filename, 'r');
            $json_string = str_replace('"','|',fread($handle, filesize($filename)));
            fclose($handle);

            // send the json to buffer (in multiple parts)
            while (strlen($json_string) > 0) {
                if (strlen($json_string) > $limit) {
                    $result = $rcon->send('ast write "'.$funcom_id.'" "'.substr($json_string, 0, $limit)).'"';
                    $json_string = substr($json_string, $limit);
                } else {
                    $result = $rcon->send('ast write "'.$funcom_id.'" "'.$json_string.'"');
                    $json_string = '';
                }
            }

            // buffer is filled, do the import
            $result = $rcon->send('ast import "'.$funcom_id.'"');

            if ($result != 'import done.') {
                // uh oh something went wrong, copy json and dump error.
                copy($filename, 'failed_'.$funcom_id.'.json');
                $filename = 'failed_'.$funcom_id.'.txt';
                $handle = fopen($filename, 'w');
                fwrite($handle, $result);
                fclose($handle);
                returnError(400, 'cannot import - rcon reply "'.$result.'"');
            } else {
                // rotate backup files if enabled
                if ($backup_rotate) {
                    for ($i = 4; $i > 0 ; $i--) {
                        $j = $i - 1;
                        $filename_dst = 'backup'.$i.'_'.$funcom_id.'.json';
                        if ($j != 0)
                            $filename_src = 'backup'.$j.'_'.$funcom_id.'.json';
                        else
                            $filename_src = 'backup_'.$funcom_id.'.json';

                        if (file_exists($filename_dst))
                            unlink($filename_dst);
                        if (file_exists($filename_src))
                            rename($filename_src, $filename_dst);
                    }
                }

                // backup the file
                rename($filename, 'backup_'.$funcom_id.'.json');
            }

            // MOD import begin
            // MOD import end
        }

         // check for claimed vote, spawn in item 11073 (skeleton key)
        if ($votes_result == 1)
            $rcon->send('ast spawn "'.$funcom_id.'" 11073');

        returnSuccess();
        break;

    case 'pong':
        $filename = 'ping-'.$server_id.'.test';
        $handle = @fopen($filename, 'w');
        if ($handle)
            fclose($handle);
        break;

    default;
        break;
}
?>

在游戏中设置连接到web服务器

1、登录管理员账号

2、点击 ~ 按键打开控制台

3、输入连接web服务器的命令

DataCmd TransferConfig "http://<webserver>/<pfad>/cluster.php" <handle>

// <handle> 为我们在cluster.ini文件中设置的游戏别名
// 如网站地址为www.darktool.cn,且进入的游戏服务器为示例一服务器则在控制台输入如下代码
// DataCmd TransferConfig "http://www.darktool.cn/cluster.php" server-exiles
// 配置成功后在web服务器的测试页面中可以看到 2-Way-Test: good

4、在游戏控制台输入以下命令查看当前的连接信息

DataCmd TransferCheck

在游戏中布置传送门

1、通过搜索 AST Placeable 刷传送门

2、放置传送门并编辑
2024-08-06T18:39:26.png

通过pyhon监控日志文件实现跨服聊天

import os
import time
import datetime
import requests
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class MyHandler(FileSystemEventHandler):
    def __init__(self, channel_name, name, url, params):
        super().__init__()
        self.last_position = 0
        self.pippiChat = '[Pippi]PippiChat:'
        self.pippiChat_l = len(self.pippiChat)
        self.said = 'said in channel [{}]:'.format(channel_name)
        self.said_l = len(self.said)
        self.name = name
        self.url = url
        self.params = params
    def on_modified(self, event):
        if not event.is_directory and event.src_path.endswith("ConanSandbox.log"):
            with open(event.src_path, "r", encoding='utf-8') as f:
                f.seek(0, 2) # 将指针移动到文件末尾
                if self.last_position == 0 or self.last_position > f.tell():
                    self.last_position = f.tell()   # 如果是第一次打开文件则将读取记录移动到文件尾
                f.seek(self.last_position, os.SEEK_SET) # 将指针移动到上次读取位置
                try:
                    for line in f.readlines():
                        #print(line)
                        pippiChat_i = line.find(self.pippiChat)
                        said_i = line.find(self.said)
                        name_i = line.find(self.name)
                        #print("{} {}:{}".format(pippiChat_i, said_i, name_i))
                        if name_i == -1 and pippiChat_i != -1 and said_i > pippiChat_i:
                            send_name = line[(pippiChat_i + self.pippiChat_l):said_i]
                            send_said = line[(said_i + self.said_l):]
                            send = "{} {}:{}".format(self.name, send_name, send_said)
                            now = datetime.datetime.now()  # 获取当前时间
                            print("[{}] {}".format(now.strftime("%Y-%m-%d %H:%M:%S"), send))
                            self.params['prm'] = send
                            requests.get(self.url, params = self.params)
                except UnicodeDecodeError as e:
                    print(e)
                    f.seek(0, 2) # 将指针移动到文件末尾  
                self.last_position = f.tell() #记录当前读取位置

if __name__ == "__main__":
    # channel_name为监控的频道名 name为跨服聊天时自定义的服务器名 url为Amunets Server Transfer的web服务器中提供的发送消息接口
    event_handler = MyHandler('世界频道', '[流放]', 'http://127.0.0.1/cluster.php', {
        'cmd': 'chat',
        'srv': 'server-exiles'
    })
    observer = Observer()
    observer.schedule(event_handler, "D:\conan-exiles\DedicatedServerLauncher\ConanExilesDedicatedServer\ConanSandbox\Saved\Logs", recursive=False)
    observer.start()

    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

效果如下,其中 [岛] 为岛图公屏发送的消息,如果是上面示例中的服务器跨服消息前会包含[流放]
2023-05-31T10:45:45.png

将程序优先级调整为实时

将程序的优先级调整为实时,解决资源调度不均衡的问题(尝试解决服务器资源大量浪费的问题,使得能容纳跟多的人数)
$processName = "ConanSandboxServer-Win64"
$processes = Get-Process | Where-Object {$_.ProcessName -like "*$processName*"}

foreach ($process in $processes) {
    Write-Host "找到进程" $process.ProcessName
    Write-Host "当前进程优先级" $processes.PriorityClass
    if ( $process.PriorityClass -eq "RealTime") {
        Write-Host "当前程序的优先级为实时,无需调整"
    } else {
        $process.PriorityClass = [System.Diagnostics.ProcessPriorityClass]::RealTime
        Write-Host "将当前程序的优先级调整为实时"
    }
}

将程序设置每隔10分钟自动执行一次

在PowerShell中,您可以使用Windows任务计划程序来定时执行脚本。以下是一些具体步骤:

打开Windows任务计划程序:单击“开始”菜单,在搜索框中键入“任务计划程序”,从搜索结果中选择“任务计划程序”。

创建一个新任务:在任务计划程序窗口中,选择“创建任务”选项,填写相关信息,例如任务名称和描述。

设置要执行的脚本:在“操作”选项卡中,点选“新建”,进入“新建操作”窗口。在该窗口中,选择PowerShell作为“程序/脚本”,输入要执行的PowerShell脚本的完整路径,并在“添加参数”字段中输入脚本所需的任何参数。

设置计划:在“触发器”选项卡中,选择要执行任务的时间和日期。如果需要更高级的计划设置,则可以选择“更改设置”并按照指示进行更改。

完成设置:在“条件”选项卡中,设置要求的条件和约束(如WiFi网络是否连接),然后单击“确定”按钮。任务计划程序将保存您的设置,并在指定的时间和日期自动运行您的PowerShell脚本。

需要注意的是,如果您的PowerShell脚本需要进行与管理员身份有关的操作,则需要以管理员身份运行任务计划程序。此外,任务计划程序还可以设置为每次启动时执行脚本、以及在计算机空闲时运行脚本等等,因此任务计划程序非常灵活和实用。

2023-05-15T06:14:22.png
2023-05-15T06:15:26.png
2023-05-15T06:16:14.png
2023-05-15T06:16:46.png
2023-05-15T06:17:14.png

最后修改:2024 年 08 月 07 日
如果觉得我的文章对你有用,请随意赞赏