跨地图传送
2305969565 Amunets Server Transfer - v1.1.18
原作者提供的使用说明
视频教程 视频为英文,听不懂没关系,可以参考视频中的流程和本教程相互印证
配置web服务器
1、到 files.arianchen.de 网站下载最新的网站压缩包(如 amunet-server-transfer_1_1_1a.zip ),将压缩包文件解压后上传web服务器,注意web服务器php版本设置为7.4版本
2、打开cluster.ini文件,将里面的配置文件改为对应服务器的ip、端口和密码
3、打开web服务器的测试页面,查看是否正确配置
http://<webserver>/<pfad>/cluster.php?cmd=test
// 如网站地址为www.darktool.cn,且cluster.php等文件存放于web服务器根目录,则输入如下地址到浏览器地址栏查看
// http://www.darktool.cn/cluster.php?cmd=test
常见错误
- 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、放置传送门并编辑
通过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()
效果如下,其中 [岛] 为岛图公屏发送的消息,如果是上面示例中的服务器跨服消息前会包含[流放]
将程序优先级调整为实时
将程序的优先级调整为实时,解决资源调度不均衡的问题(尝试解决服务器资源大量浪费的问题,使得能容纳跟多的人数)
$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脚本需要进行与管理员身份有关的操作,则需要以管理员身份运行任务计划程序。此外,任务计划程序还可以设置为每次启动时执行脚本、以及在计算机空闲时运行脚本等等,因此任务计划程序非常灵活和实用。